@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.
- package/dist/agent-loop.d.ts +38 -0
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +532 -117
- 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 +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/llm-client.d.ts +20 -2
- package/dist/llm-client.d.ts.map +1 -1
- package/dist/llm-client.js +213 -8
- package/dist/llm-client.js.map +1 -1
- package/dist/llm-orchestrator.d.ts +74 -0
- package/dist/llm-orchestrator.d.ts.map +1 -0
- package/dist/llm-orchestrator.js +144 -0
- package/dist/llm-orchestrator.js.map +1 -0
- 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/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- 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;
|
|
@@ -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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
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
|
-
|
|
2588
|
-
|
|
2885
|
+
// Collect target file paths from write tool args
|
|
2886
|
+
const writeFilePaths = writeToolCalls.flatMap((tc) => {
|
|
2589
2887
|
const args = this.parseToolArgs(tc.arguments);
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
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
|
-
//
|
|
2596
|
-
|
|
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
|
-
|
|
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);
|
|
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 —
|
|
2917
|
+
// DependencyAnalyzer failure is non-fatal — all writes run sequentially
|
|
2635
2918
|
}
|
|
2636
2919
|
}
|
|
2637
|
-
//
|
|
2638
|
-
|
|
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
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
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
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
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
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
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 (
|
|
2661
|
-
//
|
|
2662
|
-
const
|
|
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
|
-
|
|
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
|
-
}
|
|
3001
|
+
if (result?.output.startsWith('[INTERRUPTED]'))
|
|
3002
|
+
interrupted = true;
|
|
2684
3003
|
}
|
|
2685
3004
|
else {
|
|
2686
|
-
//
|
|
2687
|
-
this.emitReasoning(
|
|
2688
|
-
const
|
|
2689
|
-
for (
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
if (
|
|
2694
|
-
|
|
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
|
|
2701
|
-
name: tc
|
|
2702
|
-
output: `Error: ${
|
|
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
|
-
}
|
|
2708
|
-
}
|
|
2709
|
-
}
|
|
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
|
*/
|