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