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