@yuaone/core 0.4.2 → 0.4.4
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-logger.d.ts +1 -1
- package/dist/agent-logger.d.ts.map +1 -1
- package/dist/agent-logger.js +17 -15
- package/dist/agent-logger.js.map +1 -1
- package/dist/agent-loop.d.ts +31 -0
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +514 -98
- package/dist/agent-loop.js.map +1 -1
- package/dist/agent-modes.d.ts.map +1 -1
- package/dist/agent-modes.js +5 -0
- package/dist/agent-modes.js.map +1 -1
- package/dist/async-completion-queue.d.ts +2 -0
- package/dist/async-completion-queue.d.ts.map +1 -1
- package/dist/async-completion-queue.js +14 -0
- package/dist/async-completion-queue.js.map +1 -1
- package/dist/auto-fix.d.ts.map +1 -1
- package/dist/auto-fix.js +12 -1
- package/dist/auto-fix.js.map +1 -1
- package/dist/benchmark-runner.d.ts.map +1 -1
- package/dist/benchmark-runner.js +5 -1
- package/dist/benchmark-runner.js.map +1 -1
- package/dist/constants.d.ts +12 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +14 -0
- package/dist/constants.js.map +1 -1
- package/dist/context-manager.d.ts +25 -0
- package/dist/context-manager.d.ts.map +1 -1
- package/dist/context-manager.js +132 -5
- package/dist/context-manager.js.map +1 -1
- package/dist/continuation-engine.d.ts.map +1 -1
- package/dist/continuation-engine.js +8 -7
- package/dist/continuation-engine.js.map +1 -1
- package/dist/continuous-reflection.d.ts.map +1 -1
- package/dist/continuous-reflection.js +22 -12
- package/dist/continuous-reflection.js.map +1 -1
- package/dist/cost-optimizer.js +1 -1
- package/dist/cost-optimizer.js.map +1 -1
- package/dist/cross-file-refactor.d.ts.map +1 -1
- package/dist/cross-file-refactor.js +7 -2
- package/dist/cross-file-refactor.js.map +1 -1
- package/dist/dag-orchestrator.d.ts +10 -1
- package/dist/dag-orchestrator.d.ts.map +1 -1
- package/dist/dag-orchestrator.js +101 -6
- package/dist/dag-orchestrator.js.map +1 -1
- package/dist/debate-orchestrator.d.ts +1 -0
- package/dist/debate-orchestrator.d.ts.map +1 -1
- package/dist/debate-orchestrator.js +27 -15
- package/dist/debate-orchestrator.js.map +1 -1
- package/dist/dependency-analyzer.d.ts.map +1 -1
- package/dist/dependency-analyzer.js +19 -1
- package/dist/dependency-analyzer.js.map +1 -1
- package/dist/dynamic-role-generator.d.ts.map +1 -1
- package/dist/dynamic-role-generator.js +6 -3
- package/dist/dynamic-role-generator.js.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/event-bus.d.ts.map +1 -1
- package/dist/event-bus.js +4 -3
- package/dist/event-bus.js.map +1 -1
- package/dist/execution-engine.d.ts +39 -1
- package/dist/execution-engine.d.ts.map +1 -1
- package/dist/execution-engine.js +453 -83
- package/dist/execution-engine.js.map +1 -1
- package/dist/failure-recovery.d.ts.map +1 -1
- package/dist/failure-recovery.js +14 -3
- package/dist/failure-recovery.js.map +1 -1
- package/dist/git-intelligence.d.ts.map +1 -1
- package/dist/git-intelligence.js +16 -11
- package/dist/git-intelligence.js.map +1 -1
- package/dist/governor.d.ts +8 -0
- package/dist/governor.d.ts.map +1 -1
- package/dist/governor.js +19 -1
- package/dist/governor.js.map +1 -1
- package/dist/hierarchical-planner.d.ts +3 -0
- package/dist/hierarchical-planner.d.ts.map +1 -1
- package/dist/hierarchical-planner.js +32 -2
- package/dist/hierarchical-planner.js.map +1 -1
- package/dist/impact-analyzer.d.ts +27 -0
- package/dist/impact-analyzer.d.ts.map +1 -1
- package/dist/impact-analyzer.js +415 -53
- package/dist/impact-analyzer.js.map +1 -1
- package/dist/intent-inference.d.ts.map +1 -1
- package/dist/intent-inference.js +20 -24
- package/dist/intent-inference.js.map +1 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +5 -3
- package/dist/kernel.js.map +1 -1
- package/dist/language-detector.d.ts +19 -0
- package/dist/language-detector.d.ts.map +1 -0
- package/dist/language-detector.js +482 -0
- package/dist/language-detector.js.map +1 -0
- package/dist/language-support.d.ts.map +1 -1
- package/dist/language-support.js +5 -9
- package/dist/language-support.js.map +1 -1
- package/dist/llm-client.d.ts +21 -8
- package/dist/llm-client.d.ts.map +1 -1
- package/dist/llm-client.js +125 -21
- package/dist/llm-client.js.map +1 -1
- package/dist/mcp-client.d.ts.map +1 -1
- package/dist/mcp-client.js +9 -1
- package/dist/mcp-client.js.map +1 -1
- package/dist/memory-manager.d.ts +13 -8
- package/dist/memory-manager.d.ts.map +1 -1
- package/dist/memory-manager.js +125 -32
- package/dist/memory-manager.js.map +1 -1
- package/dist/memory-updater.d.ts.map +1 -1
- package/dist/memory-updater.js +5 -4
- package/dist/memory-updater.js.map +1 -1
- package/dist/memory.d.ts +6 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +32 -4
- package/dist/memory.js.map +1 -1
- package/dist/parallel-executor.d.ts +7 -0
- package/dist/parallel-executor.d.ts.map +1 -1
- package/dist/parallel-executor.js +28 -0
- package/dist/parallel-executor.js.map +1 -1
- package/dist/perf-optimizer.d.ts.map +1 -1
- package/dist/perf-optimizer.js +18 -3
- package/dist/perf-optimizer.js.map +1 -1
- package/dist/persona.d.ts.map +1 -1
- package/dist/persona.js +8 -3
- package/dist/persona.js.map +1 -1
- package/dist/planner.d.ts.map +1 -1
- package/dist/planner.js +5 -3
- package/dist/planner.js.map +1 -1
- package/dist/plugin-auto-loader.d.ts.map +1 -1
- package/dist/plugin-auto-loader.js +4 -1
- package/dist/plugin-auto-loader.js.map +1 -1
- package/dist/plugin-registry.d.ts +4 -0
- package/dist/plugin-registry.d.ts.map +1 -1
- package/dist/plugin-registry.js +6 -0
- package/dist/plugin-registry.js.map +1 -1
- package/dist/plugin-validator.d.ts.map +1 -1
- package/dist/plugin-validator.js +10 -1
- package/dist/plugin-validator.js.map +1 -1
- package/dist/reasoning-aggregator.d.ts +35 -0
- package/dist/reasoning-aggregator.d.ts.map +1 -0
- package/dist/reasoning-aggregator.js +102 -0
- package/dist/reasoning-aggregator.js.map +1 -0
- package/dist/reasoning-tree.d.ts +23 -0
- package/dist/reasoning-tree.d.ts.map +1 -0
- package/dist/reasoning-tree.js +44 -0
- package/dist/reasoning-tree.js.map +1 -0
- package/dist/session-persistence.d.ts +8 -4
- package/dist/session-persistence.d.ts.map +1 -1
- package/dist/session-persistence.js +22 -7
- package/dist/session-persistence.js.map +1 -1
- package/dist/skill-learner.d.ts.map +1 -1
- package/dist/skill-learner.js +4 -2
- package/dist/skill-learner.js.map +1 -1
- package/dist/skill-loader.d.ts +4 -0
- package/dist/skill-loader.d.ts.map +1 -1
- package/dist/skill-loader.js +6 -0
- package/dist/skill-loader.js.map +1 -1
- package/dist/speculative-executor.d.ts +22 -0
- package/dist/speculative-executor.d.ts.map +1 -1
- package/dist/speculative-executor.js +90 -45
- package/dist/speculative-executor.js.map +1 -1
- package/dist/state-machine.d.ts.map +1 -1
- package/dist/state-machine.js +4 -2
- package/dist/state-machine.js.map +1 -1
- package/dist/sub-agent-prompts.d.ts +5 -29
- package/dist/sub-agent-prompts.d.ts.map +1 -1
- package/dist/sub-agent-prompts.js +231 -134
- package/dist/sub-agent-prompts.js.map +1 -1
- package/dist/sub-agent.d.ts +19 -0
- package/dist/sub-agent.d.ts.map +1 -1
- package/dist/sub-agent.js +135 -11
- package/dist/sub-agent.js.map +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +45 -0
- package/dist/system-prompt.js.map +1 -1
- package/dist/task-classifier.js +1 -1
- package/dist/task-classifier.js.map +1 -1
- package/dist/types.d.ts +67 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vector-index.d.ts +14 -0
- package/dist/vector-index.d.ts.map +1 -1
- package/dist/vector-index.js +84 -16
- package/dist/vector-index.js.map +1 -1
- package/dist/workspace-lock.d.ts +5 -0
- package/dist/workspace-lock.d.ts.map +1 -0
- package/dist/workspace-lock.js +16 -0
- package/dist/workspace-lock.js.map +1 -0
- package/package.json +2 -1
package/dist/agent-loop.js
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* ContextManager가 컨텍스트 윈도우를 관리한다.
|
|
8
8
|
*/
|
|
9
9
|
import { EventEmitter } from "node:events";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
10
11
|
import { BYOKClient } from "./llm-client.js";
|
|
11
12
|
import { Governor } from "./governor.js";
|
|
12
13
|
import { ContextManager } from "./context-manager.js";
|
|
14
|
+
import { SessionPersistence } from "./session-persistence.js";
|
|
13
15
|
import { LLMError, PlanLimitError, ApprovalRequiredError, } from "./errors.js";
|
|
14
16
|
import { ApprovalManager, } from "./approval.js";
|
|
15
17
|
import { AutoFixLoop, } from "./auto-fix.js";
|
|
@@ -41,6 +43,8 @@ import { SelfDebugLoop } from "./self-debug-loop.js";
|
|
|
41
43
|
import { SkillLearner } from "./skill-learner.js";
|
|
42
44
|
import { RepoKnowledgeGraph } from "./repo-knowledge-graph.js";
|
|
43
45
|
import { BackgroundAgentManager } from "./background-agent.js";
|
|
46
|
+
import { ReasoningAggregator } from "./reasoning-aggregator.js";
|
|
47
|
+
import { ReasoningTree } from "./reasoning-tree.js";
|
|
44
48
|
/**
|
|
45
49
|
* AgentLoop — YUAN 에이전트의 핵심 실행 루프.
|
|
46
50
|
*
|
|
@@ -69,6 +73,7 @@ import { BackgroundAgentManager } from "./background-agent.js";
|
|
|
69
73
|
* ```
|
|
70
74
|
*/
|
|
71
75
|
export class AgentLoop extends EventEmitter {
|
|
76
|
+
abortSignal;
|
|
72
77
|
llmClient;
|
|
73
78
|
governor;
|
|
74
79
|
contextManager;
|
|
@@ -104,6 +109,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
104
109
|
worldState = null;
|
|
105
110
|
costOptimizer;
|
|
106
111
|
impactAnalyzer = null;
|
|
112
|
+
impactHintInjected = false;
|
|
107
113
|
selfReflection = null;
|
|
108
114
|
debateOrchestrator = null;
|
|
109
115
|
continuousReflection = null;
|
|
@@ -123,6 +129,8 @@ export class AgentLoop extends EventEmitter {
|
|
|
123
129
|
skillLearner = null;
|
|
124
130
|
repoGraph = null;
|
|
125
131
|
backgroundAgentManager = null;
|
|
132
|
+
sessionPersistence = null;
|
|
133
|
+
sessionId = null;
|
|
126
134
|
enableToolPlanning;
|
|
127
135
|
enableSkillLearning;
|
|
128
136
|
enableBackgroundAgents;
|
|
@@ -139,10 +147,70 @@ export class AgentLoop extends EventEmitter {
|
|
|
139
147
|
reasoning: 0,
|
|
140
148
|
total: 0,
|
|
141
149
|
};
|
|
150
|
+
reasoningAggregator = new ReasoningAggregator();
|
|
151
|
+
reasoningTree = new ReasoningTree();
|
|
152
|
+
resumedFromSession = false;
|
|
153
|
+
/**
|
|
154
|
+
* Restore AgentLoop state from persisted session (yuan resume)
|
|
155
|
+
*/
|
|
156
|
+
async restoreSession(data) {
|
|
157
|
+
if (!data)
|
|
158
|
+
return;
|
|
159
|
+
try {
|
|
160
|
+
this.governor.resetIteration?.();
|
|
161
|
+
if (data.snapshot?.id) {
|
|
162
|
+
this.sessionId = data.snapshot.id;
|
|
163
|
+
}
|
|
164
|
+
if (data.snapshot?.iteration) {
|
|
165
|
+
this.iterationCount = data.snapshot.iteration;
|
|
166
|
+
this.governor.restoreIteration?.(data.snapshot.iteration);
|
|
167
|
+
}
|
|
168
|
+
if (data.snapshot?.tokenUsage) {
|
|
169
|
+
const input = data.snapshot.tokenUsage.input ?? 0;
|
|
170
|
+
const output = data.snapshot.tokenUsage.output ?? 0;
|
|
171
|
+
this.tokenUsage = {
|
|
172
|
+
input,
|
|
173
|
+
output,
|
|
174
|
+
reasoning: 0,
|
|
175
|
+
total: input + output,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (Array.isArray(data.changedFiles)) {
|
|
179
|
+
this.changedFiles = data.changedFiles;
|
|
180
|
+
}
|
|
181
|
+
if (Array.isArray(data.messages) && data.messages.length > 0) {
|
|
182
|
+
this.contextManager.clear();
|
|
183
|
+
for (const msg of data.messages) {
|
|
184
|
+
this.contextManager.addMessage(msg);
|
|
185
|
+
}
|
|
186
|
+
if (!data.messages.some((msg) => msg.role === "system")) {
|
|
187
|
+
this.contextManager.addMessage({
|
|
188
|
+
role: "system",
|
|
189
|
+
content: this.config.loop.systemPrompt,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
this.activePlan = data.plan ?? null;
|
|
194
|
+
this.resumedFromSession = true;
|
|
195
|
+
this.emitEvent({
|
|
196
|
+
kind: "agent:thinking",
|
|
197
|
+
content: "Session restored.",
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
this.emitEvent({
|
|
202
|
+
kind: "agent:error",
|
|
203
|
+
message: err instanceof Error ? err.message : String(err),
|
|
204
|
+
retryable: false,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
142
208
|
constructor(options) {
|
|
143
209
|
super();
|
|
210
|
+
this.setMaxListeners(100);
|
|
144
211
|
this.config = options.config;
|
|
145
212
|
this.toolExecutor = options.toolExecutor;
|
|
213
|
+
this.abortSignal = options.abortSignal;
|
|
146
214
|
this.enableMemory = options.enableMemory !== false;
|
|
147
215
|
this.enablePlanning = options.enablePlanning !== false;
|
|
148
216
|
this.planningThreshold = options.planningThreshold ?? "moderate";
|
|
@@ -210,6 +278,13 @@ export class AgentLoop extends EventEmitter {
|
|
|
210
278
|
const projectPath = this.config.loop.projectPath;
|
|
211
279
|
if (!projectPath)
|
|
212
280
|
return;
|
|
281
|
+
// Session persistence init
|
|
282
|
+
try {
|
|
283
|
+
this.sessionPersistence = new SessionPersistence(undefined, projectPath);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
this.sessionPersistence = null;
|
|
287
|
+
}
|
|
213
288
|
let yuanMdContent;
|
|
214
289
|
let projectStructure;
|
|
215
290
|
// Memory 로드
|
|
@@ -299,10 +374,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
299
374
|
this.mcpToolDefinitions = [];
|
|
300
375
|
}
|
|
301
376
|
}
|
|
302
|
-
// HierarchicalPlanner 생성
|
|
303
|
-
if (this.enablePlanning && projectPath) {
|
|
304
|
-
this.planner = new HierarchicalPlanner({ projectPath });
|
|
305
|
-
}
|
|
306
377
|
// ReflexionEngine 생성
|
|
307
378
|
if (projectPath) {
|
|
308
379
|
this.reflexionEngine = new ReflexionEngine({ projectPath });
|
|
@@ -375,25 +446,30 @@ export class AgentLoop extends EventEmitter {
|
|
|
375
446
|
try {
|
|
376
447
|
this.skillLearner = new SkillLearner(projectPath);
|
|
377
448
|
await this.skillLearner.init();
|
|
378
|
-
// 학습된 스킬 중 관련 있는 것을 플러그인 레지스트리에 등록
|
|
379
|
-
const learnedSkills = this.skillLearner.getAllSkills();
|
|
380
|
-
if (learnedSkills.length > 0) {
|
|
381
|
-
const skillNames = learnedSkills
|
|
382
|
-
.filter((s) => s.confidence >= 0.3)
|
|
383
|
-
.map((s) => s.id);
|
|
384
|
-
if (skillNames.length > 0) {
|
|
385
|
-
this.contextManager.addMessage({
|
|
386
|
-
role: "system",
|
|
387
|
-
content: `[Learned Skills: ${skillNames.join(", ")}] — Auto-activate on matching error patterns.`,
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
449
|
}
|
|
392
450
|
catch {
|
|
393
|
-
// SkillLearner 초기화 실패는 치명적이지 않음
|
|
394
451
|
this.skillLearner = null;
|
|
395
452
|
}
|
|
396
453
|
}
|
|
454
|
+
// HierarchicalPlanner 생성
|
|
455
|
+
this.planner = new HierarchicalPlanner({ projectPath });
|
|
456
|
+
if (this.skillLearner) {
|
|
457
|
+
this.planner.setSkillLearner(this.skillLearner);
|
|
458
|
+
}
|
|
459
|
+
if (this.skillLearner) {
|
|
460
|
+
const learnedSkills = this.skillLearner.getAllSkills();
|
|
461
|
+
if (learnedSkills.length > 0) {
|
|
462
|
+
const skillNames = learnedSkills
|
|
463
|
+
.filter((s) => s.confidence >= 0.3)
|
|
464
|
+
.map((s) => s.id);
|
|
465
|
+
if (skillNames.length > 0) {
|
|
466
|
+
this.contextManager.addMessage({
|
|
467
|
+
role: "system",
|
|
468
|
+
content: `[Learned Skills: ${skillNames.join(", ")}] — Auto-activate on matching error patterns.`,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
397
473
|
// RepoKnowledgeGraph 초기화 (코드 구조 그래프 — 비동기 빌드)
|
|
398
474
|
if (projectPath) {
|
|
399
475
|
try {
|
|
@@ -450,7 +526,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
450
526
|
if (!this.continuationEngine)
|
|
451
527
|
return;
|
|
452
528
|
const checkpoint = {
|
|
453
|
-
sessionId:
|
|
529
|
+
sessionId: this.sessionId ?? "unknown-session",
|
|
454
530
|
goal: state.goal,
|
|
455
531
|
progress: {
|
|
456
532
|
completedTasks: state.completedTasks,
|
|
@@ -509,12 +585,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
509
585
|
});
|
|
510
586
|
});
|
|
511
587
|
this.continuousReflection.on("reflection:context_overflow", () => {
|
|
512
|
-
|
|
513
|
-
this.emitEvent({
|
|
514
|
-
kind: "agent:thinking",
|
|
515
|
-
content: "Context usage at 95%+ — saving state and stopping.",
|
|
516
|
-
});
|
|
517
|
-
this.abort();
|
|
588
|
+
void this.handleSoftContextOverflow();
|
|
518
589
|
});
|
|
519
590
|
}
|
|
520
591
|
/**
|
|
@@ -554,20 +625,58 @@ export class AgentLoop extends EventEmitter {
|
|
|
554
625
|
*/
|
|
555
626
|
async run(userMessage) {
|
|
556
627
|
this.aborted = false;
|
|
557
|
-
this.
|
|
558
|
-
this.
|
|
628
|
+
this.reasoningAggregator.reset();
|
|
629
|
+
this.reasoningTree.reset();
|
|
630
|
+
if (!this.resumedFromSession) {
|
|
631
|
+
this.changedFiles = [];
|
|
632
|
+
this.allToolResults = [];
|
|
633
|
+
this.iterationCount = 0;
|
|
634
|
+
this.originalSnapshots.clear();
|
|
635
|
+
this.previousStrategies = [];
|
|
636
|
+
this.activeSkillIds = [];
|
|
637
|
+
this.iterationSystemMsgCount = 0;
|
|
638
|
+
this.tokenUsage = {
|
|
639
|
+
input: 0,
|
|
640
|
+
output: 0,
|
|
641
|
+
reasoning: 0,
|
|
642
|
+
total: 0,
|
|
643
|
+
};
|
|
644
|
+
this.impactHintInjected = false;
|
|
645
|
+
}
|
|
559
646
|
this.checkpointSaved = false;
|
|
560
|
-
this.iterationCount = 0;
|
|
561
|
-
this.originalSnapshots.clear();
|
|
562
|
-
this.previousStrategies = [];
|
|
563
|
-
this.activeSkillIds = [];
|
|
564
|
-
this.iterationSystemMsgCount = 0;
|
|
565
647
|
this.failureRecovery.reset();
|
|
566
648
|
this.costOptimizer.reset();
|
|
567
649
|
this.tokenBudgetManager.reset();
|
|
568
650
|
const runStartTime = Date.now();
|
|
569
651
|
// 첫 실행 시 메모리/프로젝트 컨텍스트 자동 로드
|
|
570
652
|
await this.init();
|
|
653
|
+
if (this.sessionPersistence) {
|
|
654
|
+
if (!this.sessionId) {
|
|
655
|
+
this.sessionId = randomUUID();
|
|
656
|
+
}
|
|
657
|
+
const nowIso = new Date().toISOString();
|
|
658
|
+
const snapshot = {
|
|
659
|
+
id: this.sessionId,
|
|
660
|
+
createdAt: nowIso,
|
|
661
|
+
updatedAt: nowIso,
|
|
662
|
+
workDir: this.config.loop.projectPath ?? "",
|
|
663
|
+
provider: String(this.config.byok.provider ?? "unknown"),
|
|
664
|
+
model: String(this.config.byok.model ?? "unknown"),
|
|
665
|
+
status: "running",
|
|
666
|
+
iteration: this.iterationCount,
|
|
667
|
+
tokenUsage: {
|
|
668
|
+
input: this.tokenUsage.input,
|
|
669
|
+
output: this.tokenUsage.output,
|
|
670
|
+
},
|
|
671
|
+
messageCount: this.contextManager.getMessages().length,
|
|
672
|
+
};
|
|
673
|
+
await this.sessionPersistence.save(this.sessionId, {
|
|
674
|
+
snapshot,
|
|
675
|
+
messages: this.contextManager.getMessages(),
|
|
676
|
+
plan: this.activePlan,
|
|
677
|
+
changedFiles: this.changedFiles,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
571
680
|
// 사용자 입력 검증 (prompt injection 방어)
|
|
572
681
|
const inputValidation = this.promptDefense.validateUserInput(userMessage);
|
|
573
682
|
if (inputValidation.injectionDetected && (inputValidation.severity === "critical" || inputValidation.severity === "high")) {
|
|
@@ -653,11 +762,18 @@ export class AgentLoop extends EventEmitter {
|
|
|
653
762
|
this.continuousReflection.stop();
|
|
654
763
|
}
|
|
655
764
|
}
|
|
765
|
+
if (this.sessionPersistence && this.sessionId) {
|
|
766
|
+
const finalStatus = result.reason === "ERROR"
|
|
767
|
+
? "crashed"
|
|
768
|
+
: "completed";
|
|
769
|
+
await this.sessionPersistence.updateStatus(this.sessionId, finalStatus);
|
|
770
|
+
}
|
|
656
771
|
// 실행 완료 후 메모리 자동 업데이트
|
|
657
772
|
await this.updateMemoryAfterRun(userMessage, result, Date.now() - runStartTime);
|
|
658
773
|
// SkillLearner: 성공적 에러 해결 시 새로운 스킬 학습
|
|
659
774
|
if (this.skillLearner && result.reason === "GOAL_ACHIEVED") {
|
|
660
775
|
try {
|
|
776
|
+
let newSkillId = null;
|
|
661
777
|
const errorToolResults = this.allToolResults.filter((r) => !r.success);
|
|
662
778
|
if (errorToolResults.length > 0 && this.changedFiles.length > 0) {
|
|
663
779
|
// 에러가 있었지만 결국 성공 → 학습 가능한 패턴
|
|
@@ -675,8 +791,17 @@ export class AgentLoop extends EventEmitter {
|
|
|
675
791
|
durationMs: Date.now() - runStartTime,
|
|
676
792
|
iterations: this.iterationCount,
|
|
677
793
|
});
|
|
678
|
-
this.skillLearner.extractSkillFromRun(runAnalysis, `session-${Date.now()}`);
|
|
794
|
+
const learned = this.skillLearner.extractSkillFromRun(runAnalysis, this.sessionId ?? `session-${Date.now()}`);
|
|
795
|
+
if (learned) {
|
|
796
|
+
newSkillId = learned.id;
|
|
797
|
+
}
|
|
679
798
|
await this.skillLearner.save();
|
|
799
|
+
if (newSkillId) {
|
|
800
|
+
this.emitEvent({
|
|
801
|
+
kind: "agent:thinking",
|
|
802
|
+
content: `Learned new skill: ${newSkillId}`,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
680
805
|
}
|
|
681
806
|
}
|
|
682
807
|
catch {
|
|
@@ -758,7 +883,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
758
883
|
try {
|
|
759
884
|
const entry = this.reflexionEngine.reflect({
|
|
760
885
|
goal: userGoal,
|
|
761
|
-
runId:
|
|
886
|
+
runId: randomUUID(),
|
|
762
887
|
termination: result,
|
|
763
888
|
toolResults: this.allToolResults,
|
|
764
889
|
messages: this.contextManager.getMessages(),
|
|
@@ -863,7 +988,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
863
988
|
contextUsagePercent: this.config.loop.totalTokenBudget > 0
|
|
864
989
|
? this.tokenUsage.total / this.config.loop.totalTokenBudget
|
|
865
990
|
: 0,
|
|
866
|
-
sessionId:
|
|
991
|
+
sessionId: this.sessionId ?? "unknown-session",
|
|
867
992
|
totalTokensUsed: this.tokenUsage.total,
|
|
868
993
|
workingMemory: this.buildWorkingMemorySummary(),
|
|
869
994
|
completedTasks: progress.completedTasks,
|
|
@@ -927,32 +1052,27 @@ export class AgentLoop extends EventEmitter {
|
|
|
927
1052
|
if ((complexityOrder[complexity] ?? 0) < thresholdOrder[this.planningThreshold]) {
|
|
928
1053
|
return;
|
|
929
1054
|
}
|
|
930
|
-
this.
|
|
931
|
-
kind: "agent:thinking",
|
|
932
|
-
content: `Task complexity: ${complexity}. Creating execution plan...`,
|
|
933
|
-
});
|
|
1055
|
+
this.emitSubagent("planner", "start", `task complexity ${complexity}. creating execution plan`);
|
|
934
1056
|
try {
|
|
935
1057
|
const plan = await this.planner.createHierarchicalPlan(userMessage, this.llmClient);
|
|
936
1058
|
this.activePlan = plan;
|
|
937
1059
|
this.currentTaskIndex = 0;
|
|
938
|
-
// Estimate planner token usage (plan creation typically uses ~500 tokens per task)
|
|
939
1060
|
const planTokenEstimate = plan.tactical.length * 500;
|
|
940
1061
|
this.tokenBudgetManager.recordUsage("planner", planTokenEstimate, planTokenEstimate);
|
|
941
|
-
// 계획을 컨텍스트에 주입 (LLM이 따라갈 수 있도록)
|
|
942
1062
|
const planContext = this.formatPlanForContext(plan);
|
|
943
1063
|
this.contextManager.addMessage({
|
|
944
1064
|
role: "system",
|
|
945
1065
|
content: planContext,
|
|
946
1066
|
});
|
|
1067
|
+
this.emitSubagent("planner", "done", `plan created: ${plan.tactical.length} tasks, ${plan.totalEstimatedIterations} estimated iterations, risk ${plan.strategic.riskAssessment.level}`);
|
|
1068
|
+
}
|
|
1069
|
+
catch (err) {
|
|
947
1070
|
this.emitEvent({
|
|
948
|
-
kind: "agent:
|
|
949
|
-
|
|
1071
|
+
kind: "agent:error",
|
|
1072
|
+
message: `Planning failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1073
|
+
retryable: false,
|
|
950
1074
|
});
|
|
951
1075
|
}
|
|
952
|
-
catch {
|
|
953
|
-
// 플래닝 실패는 치명적이지 않음 — LLM이 직접 처리하도록 폴백
|
|
954
|
-
this.activePlan = null;
|
|
955
|
-
}
|
|
956
1076
|
}
|
|
957
1077
|
/**
|
|
958
1078
|
* 사용자 메시지에서 태스크 복잡도를 휴리스틱으로 추정.
|
|
@@ -1121,8 +1241,17 @@ export class AgentLoop extends EventEmitter {
|
|
|
1121
1241
|
}
|
|
1122
1242
|
// ─── Core Loop ───
|
|
1123
1243
|
async executeLoop() {
|
|
1124
|
-
let iteration =
|
|
1244
|
+
let iteration = this.iterationCount;
|
|
1125
1245
|
while (!this.aborted) {
|
|
1246
|
+
// ensure loop state always matches persisted iteration
|
|
1247
|
+
if (iteration !== this.iterationCount) {
|
|
1248
|
+
iteration = this.iterationCount;
|
|
1249
|
+
}
|
|
1250
|
+
if (this.abortSignal?.aborted) {
|
|
1251
|
+
return { reason: "USER_CANCELLED" };
|
|
1252
|
+
}
|
|
1253
|
+
if (iteration === 0)
|
|
1254
|
+
this.emitReasoning("starting agent loop");
|
|
1126
1255
|
// Interrupt: pause 상태이면 resume될 때까지 대기
|
|
1127
1256
|
if (this.interruptManager.isPaused()) {
|
|
1128
1257
|
await this.waitForResume();
|
|
@@ -1146,7 +1275,22 @@ export class AgentLoop extends EventEmitter {
|
|
|
1146
1275
|
iteration++;
|
|
1147
1276
|
this.iterationCount = iteration;
|
|
1148
1277
|
const iterationStart = Date.now();
|
|
1278
|
+
this.emitReasoning(`iteration ${iteration}: preparing context`);
|
|
1149
1279
|
this.iterationSystemMsgCount = 0; // Reset per-iteration system message counter
|
|
1280
|
+
// Soft context rollover:
|
|
1281
|
+
// checkpoint first, then let ContextManager compact instead of aborting/throwing.
|
|
1282
|
+
const contextUsageRatio = this.contextManager.getUsageRatio();
|
|
1283
|
+
if (contextUsageRatio >= 0.85) {
|
|
1284
|
+
if (!this.checkpointSaved) {
|
|
1285
|
+
await this.saveAutoCheckpoint(iteration);
|
|
1286
|
+
this.checkpointSaved = true;
|
|
1287
|
+
}
|
|
1288
|
+
this.emitEvent({
|
|
1289
|
+
kind: "agent:thinking",
|
|
1290
|
+
content: `High context pressure detected (${Math.round(contextUsageRatio * 100)}%). ` +
|
|
1291
|
+
`Compressing conversation state and continuing.`,
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1150
1294
|
// Check file-pattern skill triggers based on changed files
|
|
1151
1295
|
// Guard: skip skill injection if over 80% token budget to preserve remaining budget
|
|
1152
1296
|
// Guard: max 3 active skills globally (Context Budget Rules)
|
|
@@ -1189,10 +1333,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
1189
1333
|
// Try rebalancing to free up budget from idle roles
|
|
1190
1334
|
this.tokenBudgetManager.rebalance();
|
|
1191
1335
|
}
|
|
1192
|
-
this.
|
|
1193
|
-
kind: "agent:thinking",
|
|
1194
|
-
content: `Iteration ${iteration}...`,
|
|
1195
|
-
});
|
|
1336
|
+
this.emitReasoning(`iteration ${iteration}: calling model`);
|
|
1196
1337
|
let response;
|
|
1197
1338
|
try {
|
|
1198
1339
|
response = await this.callLLMStreaming(messages);
|
|
@@ -1237,6 +1378,11 @@ export class AgentLoop extends EventEmitter {
|
|
|
1237
1378
|
// 3. 응답 처리
|
|
1238
1379
|
if (response.toolCalls.length === 0) {
|
|
1239
1380
|
const content = response.content ?? "";
|
|
1381
|
+
let finalSummary = content || "Task completed.";
|
|
1382
|
+
const finalImpactSummary = await this.buildFinalImpactSummary();
|
|
1383
|
+
if (finalImpactSummary) {
|
|
1384
|
+
finalSummary = `${finalSummary}\n\n${finalImpactSummary}`;
|
|
1385
|
+
}
|
|
1240
1386
|
// Level 2: Deep verification before declaring completion
|
|
1241
1387
|
if (this.selfReflection && this.changedFiles.length > 0) {
|
|
1242
1388
|
try {
|
|
@@ -1272,10 +1418,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
1272
1418
|
role: "system",
|
|
1273
1419
|
content: `[Self-Reflection L2] Verification failed (score: ${deepResult.overallScore}, confidence: ${deepResult.confidence.toFixed(2)}). ${deepResult.selfCritique}${issuesList ? ` Issues: ${issuesList}` : ""}. Please address these before completing.`,
|
|
1274
1420
|
});
|
|
1275
|
-
this.
|
|
1276
|
-
kind: "agent:thinking",
|
|
1277
|
-
content: `Deep verification failed (score: ${deepResult.overallScore}). Continuing to address issues...`,
|
|
1278
|
-
});
|
|
1421
|
+
this.emitSubagent("verifier", "done", `deep verification failed, score ${deepResult.overallScore}. continuing to address issues`);
|
|
1279
1422
|
continue; // Don't return GOAL_ACHIEVED, continue the loop
|
|
1280
1423
|
}
|
|
1281
1424
|
// Level 3: Multi-agent debate for complex/massive tasks
|
|
@@ -1283,10 +1426,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
1283
1426
|
["complex", "massive"].includes(this.currentComplexity) &&
|
|
1284
1427
|
deepResult.verdict !== "pass") {
|
|
1285
1428
|
try {
|
|
1286
|
-
this.
|
|
1287
|
-
kind: "agent:thinking",
|
|
1288
|
-
content: `Triggering multi-agent debate for ${this.currentComplexity} task verification...`,
|
|
1289
|
-
});
|
|
1429
|
+
this.emitSubagent("reviewer", "start", `starting debate for ${this.currentComplexity} task verification`);
|
|
1290
1430
|
const debateContext = [
|
|
1291
1431
|
`Changed files: ${this.changedFiles.join(", ")}`,
|
|
1292
1432
|
`Self-reflection score: ${deepResult.overallScore}/100`,
|
|
@@ -1314,10 +1454,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
1314
1454
|
});
|
|
1315
1455
|
continue; // Continue loop to address debate feedback
|
|
1316
1456
|
}
|
|
1317
|
-
this.
|
|
1318
|
-
kind: "agent:thinking",
|
|
1319
|
-
content: `Debate passed (score: ${debateResult.finalScore}). Proceeding to completion.`,
|
|
1320
|
-
});
|
|
1457
|
+
this.emitSubagent("reviewer", "done", `debate failed, score ${debateResult.finalScore}. continuing to address issues`);
|
|
1321
1458
|
}
|
|
1322
1459
|
catch {
|
|
1323
1460
|
// Debate failure is non-fatal — proceed with completion
|
|
@@ -1340,17 +1477,21 @@ export class AgentLoop extends EventEmitter {
|
|
|
1340
1477
|
if (content) {
|
|
1341
1478
|
this.contextManager.addMessage({
|
|
1342
1479
|
role: "assistant",
|
|
1343
|
-
content,
|
|
1480
|
+
content: finalSummary,
|
|
1344
1481
|
});
|
|
1345
1482
|
}
|
|
1346
1483
|
this.emitEvent({
|
|
1347
1484
|
kind: "agent:completed",
|
|
1348
|
-
summary:
|
|
1349
|
-
filesChanged:
|
|
1485
|
+
summary: finalSummary,
|
|
1486
|
+
filesChanged: this.changedFiles
|
|
1487
|
+
});
|
|
1488
|
+
this.emitEvent({
|
|
1489
|
+
kind: "agent:reasoning_tree",
|
|
1490
|
+
tree: this.reasoningTree.toJSON(),
|
|
1350
1491
|
});
|
|
1351
1492
|
return {
|
|
1352
1493
|
reason: "GOAL_ACHIEVED",
|
|
1353
|
-
summary:
|
|
1494
|
+
summary: finalSummary,
|
|
1354
1495
|
};
|
|
1355
1496
|
}
|
|
1356
1497
|
// 어시스턴트 메시지 저장 (tool_calls 포함)
|
|
@@ -1359,10 +1500,12 @@ export class AgentLoop extends EventEmitter {
|
|
|
1359
1500
|
content: response.content,
|
|
1360
1501
|
tool_calls: response.toolCalls,
|
|
1361
1502
|
});
|
|
1362
|
-
if (response.
|
|
1503
|
+
if (response.toolCalls.length > 1 && iteration === this.iterationCount) {
|
|
1504
|
+
const batchId = `batch_${Date.now()}`;
|
|
1363
1505
|
this.emitEvent({
|
|
1364
|
-
kind: "agent:
|
|
1365
|
-
|
|
1506
|
+
kind: "agent:tool_batch",
|
|
1507
|
+
batchId,
|
|
1508
|
+
size: response.toolCalls.length,
|
|
1366
1509
|
});
|
|
1367
1510
|
}
|
|
1368
1511
|
// 4. 도구 실행
|
|
@@ -1407,9 +1550,23 @@ export class AgentLoop extends EventEmitter {
|
|
|
1407
1550
|
tokensUsed: response.usage.input + response.usage.output,
|
|
1408
1551
|
durationMs: Date.now() - iterationStart,
|
|
1409
1552
|
});
|
|
1553
|
+
// session checkpoint
|
|
1554
|
+
if (this.sessionPersistence && this.sessionId) {
|
|
1555
|
+
const checkpoint = {
|
|
1556
|
+
iteration,
|
|
1557
|
+
tokenUsage: {
|
|
1558
|
+
input: this.tokenUsage.input,
|
|
1559
|
+
output: this.tokenUsage.output,
|
|
1560
|
+
},
|
|
1561
|
+
timestamp: new Date().toISOString(),
|
|
1562
|
+
changedFiles: this.changedFiles,
|
|
1563
|
+
};
|
|
1564
|
+
await this.sessionPersistence.checkpoint(this.sessionId, checkpoint);
|
|
1565
|
+
}
|
|
1410
1566
|
// Level 1: Quick verification after every 3rd iteration
|
|
1411
1567
|
if (this.selfReflection && iteration % 3 === 0) {
|
|
1412
1568
|
try {
|
|
1569
|
+
this.emitSubagent("verifier", "start", "running quick verification");
|
|
1413
1570
|
const changedFilesMap = new Map();
|
|
1414
1571
|
for (const filePath of this.changedFiles) {
|
|
1415
1572
|
// Collect changed file contents from tool results
|
|
@@ -1437,10 +1594,10 @@ export class AgentLoop extends EventEmitter {
|
|
|
1437
1594
|
role: "system",
|
|
1438
1595
|
content: `[Self-Reflection L1] Issues detected: ${issues.join(", ")}. Confidence: ${quickResult.confidence}`,
|
|
1439
1596
|
});
|
|
1440
|
-
this.
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
});
|
|
1597
|
+
this.emitSubagent("verifier", "done", `quick verification flagged ${issues.length} issues, confidence ${quickResult.confidence.toFixed(2)}`);
|
|
1598
|
+
}
|
|
1599
|
+
else {
|
|
1600
|
+
this.emitSubagent("verifier", "done", `quick verification passed, confidence ${quickResult.confidence.toFixed(2)}`);
|
|
1444
1601
|
}
|
|
1445
1602
|
}
|
|
1446
1603
|
}
|
|
@@ -1480,8 +1637,9 @@ export class AgentLoop extends EventEmitter {
|
|
|
1480
1637
|
// 롤백 시도
|
|
1481
1638
|
await this.failureRecovery.executeRollback(this.changedFiles, this.originalSnapshots);
|
|
1482
1639
|
this.emitEvent({
|
|
1483
|
-
kind: "agent:
|
|
1484
|
-
|
|
1640
|
+
kind: "agent:reasoning_delta",
|
|
1641
|
+
text: `rollback executed: ${decision.reason}`,
|
|
1642
|
+
source: "agent",
|
|
1485
1643
|
});
|
|
1486
1644
|
}
|
|
1487
1645
|
else if (this.iterationSystemMsgCount < 5) {
|
|
@@ -1605,6 +1763,13 @@ export class AgentLoop extends EventEmitter {
|
|
|
1605
1763
|
// reflection 체크포인트 실패는 치명적이지 않음
|
|
1606
1764
|
}
|
|
1607
1765
|
}
|
|
1766
|
+
// complex/massive task + 다중 변경 파일일 때만 aggregate impact hint 1회 주입
|
|
1767
|
+
if (!this.impactHintInjected &&
|
|
1768
|
+
this.impactAnalyzer &&
|
|
1769
|
+
this.changedFiles.length >= 2 &&
|
|
1770
|
+
(this.currentComplexity === "complex" || this.currentComplexity === "massive")) {
|
|
1771
|
+
await this.maybeInjectAggregateImpactHint();
|
|
1772
|
+
}
|
|
1608
1773
|
// 예산 초과 체크
|
|
1609
1774
|
if (this.tokenUsage.total >= this.config.loop.totalTokenBudget) {
|
|
1610
1775
|
return {
|
|
@@ -1651,6 +1816,26 @@ export class AgentLoop extends EventEmitter {
|
|
|
1651
1816
|
for await (const chunk of stream) {
|
|
1652
1817
|
if (this.aborted)
|
|
1653
1818
|
break;
|
|
1819
|
+
if (chunk.type === "reasoning" && chunk.reasoning?.text) {
|
|
1820
|
+
const aggregated = this.reasoningAggregator.push(chunk.reasoning.text, {
|
|
1821
|
+
id: chunk.reasoning.id,
|
|
1822
|
+
provider: chunk.reasoning.provider,
|
|
1823
|
+
model: chunk.reasoning.model,
|
|
1824
|
+
source: chunk.reasoning.source ?? "llm",
|
|
1825
|
+
});
|
|
1826
|
+
for (const item of aggregated) {
|
|
1827
|
+
this.reasoningTree.add("llm", item.text);
|
|
1828
|
+
this.emitEvent({
|
|
1829
|
+
kind: "agent:reasoning_delta",
|
|
1830
|
+
id: item.id,
|
|
1831
|
+
text: item.text,
|
|
1832
|
+
provider: item.provider,
|
|
1833
|
+
model: item.model,
|
|
1834
|
+
source: item.source ?? "llm",
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
continue;
|
|
1838
|
+
}
|
|
1654
1839
|
switch (chunk.type) {
|
|
1655
1840
|
case "text":
|
|
1656
1841
|
if (chunk.text) {
|
|
@@ -1668,6 +1853,16 @@ export class AgentLoop extends EventEmitter {
|
|
|
1668
1853
|
case "tool_call":
|
|
1669
1854
|
// tool_call 전에 남은 텍스트 flush
|
|
1670
1855
|
flushTextBuffer();
|
|
1856
|
+
for (const item of this.reasoningAggregator.flush()) {
|
|
1857
|
+
this.emitEvent({
|
|
1858
|
+
kind: "agent:reasoning_delta",
|
|
1859
|
+
id: item.id,
|
|
1860
|
+
text: item.text,
|
|
1861
|
+
provider: item.provider,
|
|
1862
|
+
model: item.model,
|
|
1863
|
+
source: item.source ?? "llm",
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1671
1866
|
if (chunk.toolCall) {
|
|
1672
1867
|
toolCalls.push(chunk.toolCall);
|
|
1673
1868
|
this.emitEvent({
|
|
@@ -1678,14 +1873,39 @@ export class AgentLoop extends EventEmitter {
|
|
|
1678
1873
|
}
|
|
1679
1874
|
break;
|
|
1680
1875
|
case "done":
|
|
1876
|
+
for (const item of this.reasoningAggregator.flush()) {
|
|
1877
|
+
this.emitEvent({
|
|
1878
|
+
kind: "agent:reasoning_delta",
|
|
1879
|
+
id: item.id,
|
|
1880
|
+
text: item.text,
|
|
1881
|
+
provider: item.provider,
|
|
1882
|
+
model: item.model,
|
|
1883
|
+
source: item.source ?? "llm",
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1681
1886
|
if (chunk.usage) {
|
|
1682
|
-
usage =
|
|
1887
|
+
usage = {
|
|
1888
|
+
input: chunk.usage.input ?? 0,
|
|
1889
|
+
output: chunk.usage.output ?? 0,
|
|
1890
|
+
};
|
|
1683
1891
|
}
|
|
1684
1892
|
break;
|
|
1685
1893
|
}
|
|
1686
1894
|
}
|
|
1687
1895
|
// 스트림 종료 후 남은 버퍼 flush
|
|
1688
1896
|
flushTextBuffer();
|
|
1897
|
+
for (const item of this.reasoningAggregator.flush()) {
|
|
1898
|
+
this.emitEvent({
|
|
1899
|
+
kind: "agent:reasoning_delta",
|
|
1900
|
+
id: item.id,
|
|
1901
|
+
text: item.text,
|
|
1902
|
+
provider: item.provider,
|
|
1903
|
+
model: item.model,
|
|
1904
|
+
source: item.source ?? "llm",
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
if (flushTimer)
|
|
1908
|
+
clearTimeout(flushTimer);
|
|
1689
1909
|
return {
|
|
1690
1910
|
content: content || null,
|
|
1691
1911
|
toolCalls,
|
|
@@ -1702,9 +1922,14 @@ export class AgentLoop extends EventEmitter {
|
|
|
1702
1922
|
* 4. AutoFixLoop 결과 검증 → 실패 시 에러 피드백 메시지 추가
|
|
1703
1923
|
*/
|
|
1704
1924
|
async executeTools(toolCalls) {
|
|
1925
|
+
if (toolCalls.length > 1)
|
|
1926
|
+
this.emitReasoning(`parallel tool batch started (${toolCalls.length} tools)`);
|
|
1705
1927
|
const results = [];
|
|
1706
1928
|
const deferredFixPrompts = [];
|
|
1707
1929
|
for (const toolCall of toolCalls) {
|
|
1930
|
+
const args = this.parseToolArgs(toolCall.arguments);
|
|
1931
|
+
const allDefinitions = [...this.config.loop.tools, ...this.mcpToolDefinitions];
|
|
1932
|
+
const matchedDefinition = allDefinitions.find((t) => t.name === toolCall.name);
|
|
1708
1933
|
// Governor: 안전성 검증
|
|
1709
1934
|
try {
|
|
1710
1935
|
this.governor.validateToolCall(toolCall);
|
|
@@ -1712,7 +1937,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
1712
1937
|
catch (err) {
|
|
1713
1938
|
if (err instanceof ApprovalRequiredError) {
|
|
1714
1939
|
// Governor가 위험 감지 → ApprovalManager로 승인 프로세스 위임
|
|
1715
|
-
const args = this.parseToolArgs(toolCall.arguments);
|
|
1716
1940
|
const approvalResult = await this.handleApproval(toolCall, args, err);
|
|
1717
1941
|
if (approvalResult) {
|
|
1718
1942
|
results.push(approvalResult);
|
|
@@ -1720,12 +1944,32 @@ export class AgentLoop extends EventEmitter {
|
|
|
1720
1944
|
}
|
|
1721
1945
|
// 승인됨 → 계속 실행
|
|
1722
1946
|
}
|
|
1947
|
+
// Generic tool-definition approval gate
|
|
1948
|
+
if (matchedDefinition?.requiresApproval) {
|
|
1949
|
+
const definitionApprovalReq = {
|
|
1950
|
+
id: `definition-approval-${toolCall.id}`,
|
|
1951
|
+
toolName: toolCall.name,
|
|
1952
|
+
arguments: args,
|
|
1953
|
+
reason: matchedDefinition.source === "mcp"
|
|
1954
|
+
? `MCP tool "${toolCall.name}" requires approval`
|
|
1955
|
+
: `Tool "${toolCall.name}" requires approval`,
|
|
1956
|
+
riskLevel: matchedDefinition.riskLevel === "critical" ||
|
|
1957
|
+
matchedDefinition.riskLevel === "high"
|
|
1958
|
+
? "high"
|
|
1959
|
+
: "medium",
|
|
1960
|
+
timeout: 120_000,
|
|
1961
|
+
};
|
|
1962
|
+
const definitionApprovalResult = await this.handleApprovalRequest(toolCall, definitionApprovalReq);
|
|
1963
|
+
if (definitionApprovalResult) {
|
|
1964
|
+
results.push(definitionApprovalResult);
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1723
1968
|
else {
|
|
1724
1969
|
throw err;
|
|
1725
1970
|
}
|
|
1726
1971
|
}
|
|
1727
1972
|
// ApprovalManager: 추가 승인 체크 (Governor가 못 잡은 규칙)
|
|
1728
|
-
const args = this.parseToolArgs(toolCall.arguments);
|
|
1729
1973
|
const approvalRequest = this.approvalManager.checkApproval(toolCall.name, args);
|
|
1730
1974
|
if (approvalRequest) {
|
|
1731
1975
|
const approvalResult = await this.handleApprovalRequest(toolCall, approvalRequest);
|
|
@@ -1770,15 +2014,57 @@ export class AgentLoop extends EventEmitter {
|
|
|
1770
2014
|
: mcpResult.output,
|
|
1771
2015
|
durationMs: mcpResult.durationMs,
|
|
1772
2016
|
});
|
|
2017
|
+
this.emitEvent({
|
|
2018
|
+
kind: "agent:reasoning_delta",
|
|
2019
|
+
text: `tool finished: ${toolCall.name}`,
|
|
2020
|
+
});
|
|
1773
2021
|
continue;
|
|
1774
2022
|
}
|
|
1775
2023
|
// 도구 실행 — AbortController를 InterruptManager에 등록
|
|
1776
2024
|
const startTime = Date.now();
|
|
1777
2025
|
const toolAbort = new AbortController();
|
|
1778
2026
|
this.interruptManager.registerToolAbort(toolAbort);
|
|
2027
|
+
// rollback용 원본 스냅샷은 실행 전에 저장
|
|
2028
|
+
if (["file_write", "file_edit"].includes(toolCall.name)) {
|
|
2029
|
+
const candidatePath = args.path ??
|
|
2030
|
+
args.file;
|
|
2031
|
+
if (candidatePath) {
|
|
2032
|
+
const filePathStr = String(candidatePath);
|
|
2033
|
+
if (!this.originalSnapshots.has(filePathStr)) {
|
|
2034
|
+
try {
|
|
2035
|
+
const { readFile } = await import("node:fs/promises");
|
|
2036
|
+
const original = await readFile(filePathStr, "utf-8");
|
|
2037
|
+
this.originalSnapshots.set(filePathStr, original);
|
|
2038
|
+
}
|
|
2039
|
+
catch (err) {
|
|
2040
|
+
if (err.code !== "ENOENT") {
|
|
2041
|
+
throw err;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
1779
2047
|
try {
|
|
1780
|
-
const result = await this.toolExecutor.execute(toolCall, toolAbort
|
|
2048
|
+
const result = await this.toolExecutor.execute(toolCall, toolAbort?.signal);
|
|
1781
2049
|
this.interruptManager.clearToolAbort();
|
|
2050
|
+
// Learned skill confidence feedback
|
|
2051
|
+
// 성공한 실행 결과/도구명/출력에 매칭되는 learned skill이 있으면 confidence 갱신
|
|
2052
|
+
if (this.skillLearner) {
|
|
2053
|
+
try {
|
|
2054
|
+
const relevantSkills = this.skillLearner.getRelevantSkills({
|
|
2055
|
+
errorMessage: `${toolCall.name}\n${result.output}`,
|
|
2056
|
+
filePath: typeof args.path === "string"
|
|
2057
|
+
? args.path
|
|
2058
|
+
: typeof args.file === "string"
|
|
2059
|
+
? args.file
|
|
2060
|
+
: undefined,
|
|
2061
|
+
});
|
|
2062
|
+
for (const skill of relevantSkills.slice(0, 1)) {
|
|
2063
|
+
this.skillLearner.updateConfidence(skill.id, result.success);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
catch { }
|
|
2067
|
+
}
|
|
1782
2068
|
results.push(result);
|
|
1783
2069
|
this.emitEvent({
|
|
1784
2070
|
kind: "agent:tool_result",
|
|
@@ -1788,6 +2074,8 @@ export class AgentLoop extends EventEmitter {
|
|
|
1788
2074
|
: result.output,
|
|
1789
2075
|
durationMs: result.durationMs,
|
|
1790
2076
|
});
|
|
2077
|
+
this.emitReasoning(`success: ${toolCall.name}`);
|
|
2078
|
+
this.reasoningTree.add("tool", `success: ${toolCall.name}`);
|
|
1791
2079
|
// 파일 변경 이벤트 + 추적
|
|
1792
2080
|
if (["file_write", "file_edit"].includes(toolCall.name) &&
|
|
1793
2081
|
result.success) {
|
|
@@ -1798,17 +2086,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
1798
2086
|
// 변경 파일 추적 (메모리 업데이트용)
|
|
1799
2087
|
if (!this.changedFiles.includes(filePathStr)) {
|
|
1800
2088
|
this.changedFiles.push(filePathStr);
|
|
1801
|
-
// 원본 스냅샷 저장 (rollback용) — 최초 변경 시에만
|
|
1802
|
-
if (!this.originalSnapshots.has(filePathStr)) {
|
|
1803
|
-
try {
|
|
1804
|
-
const { readFile } = await import("node:fs/promises");
|
|
1805
|
-
const original = await readFile(filePathStr, "utf-8");
|
|
1806
|
-
this.originalSnapshots.set(filePathStr, original);
|
|
1807
|
-
}
|
|
1808
|
-
catch {
|
|
1809
|
-
// 파일이 새로 생성된 경우 스냅샷 없음
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
2089
|
}
|
|
1813
2090
|
this.emitEvent({
|
|
1814
2091
|
kind: "agent:file_change",
|
|
@@ -1858,6 +2135,23 @@ export class AgentLoop extends EventEmitter {
|
|
|
1858
2135
|
break;
|
|
1859
2136
|
}
|
|
1860
2137
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2138
|
+
// Learned skill confidence feedback (failure path)
|
|
2139
|
+
if (this.skillLearner) {
|
|
2140
|
+
try {
|
|
2141
|
+
const relevantSkills = this.skillLearner.getRelevantSkills({
|
|
2142
|
+
errorMessage: `${toolCall.name}\n${errorMessage}`,
|
|
2143
|
+
filePath: typeof args.path === "string"
|
|
2144
|
+
? args.path
|
|
2145
|
+
: typeof args.file === "string"
|
|
2146
|
+
? args.file
|
|
2147
|
+
: undefined,
|
|
2148
|
+
});
|
|
2149
|
+
for (const skill of relevantSkills.slice(0, 1)) {
|
|
2150
|
+
this.skillLearner.updateConfidence(skill.id, false);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
catch { }
|
|
2154
|
+
}
|
|
1861
2155
|
results.push({
|
|
1862
2156
|
tool_call_id: toolCall.id,
|
|
1863
2157
|
name: toolCall.name,
|
|
@@ -1870,6 +2164,11 @@ export class AgentLoop extends EventEmitter {
|
|
|
1870
2164
|
message: `Tool ${toolCall.name} failed: ${errorMessage}`,
|
|
1871
2165
|
retryable: true,
|
|
1872
2166
|
});
|
|
2167
|
+
this.emitEvent({
|
|
2168
|
+
kind: "agent:reasoning_delta",
|
|
2169
|
+
text: `failed: ${toolCall.name}`,
|
|
2170
|
+
});
|
|
2171
|
+
this.reasoningTree.add("tool", `failed: ${toolCall.name}`);
|
|
1873
2172
|
}
|
|
1874
2173
|
}
|
|
1875
2174
|
return { results, deferredFixPrompts };
|
|
@@ -1880,7 +2179,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
1880
2179
|
*/
|
|
1881
2180
|
async handleApproval(toolCall, args, err) {
|
|
1882
2181
|
const request = {
|
|
1883
|
-
id:
|
|
2182
|
+
id: randomUUID(),
|
|
1884
2183
|
toolName: toolCall.name,
|
|
1885
2184
|
arguments: args,
|
|
1886
2185
|
riskLevel: "high",
|
|
@@ -1917,8 +2216,8 @@ export class AgentLoop extends EventEmitter {
|
|
|
1917
2216
|
* 실패 시 수정 프롬프트를 대화 히스토리에 추가.
|
|
1918
2217
|
*/
|
|
1919
2218
|
async validateAndFeedback(toolName, result) {
|
|
1920
|
-
// file_write/file_edit만 검증
|
|
1921
|
-
if (!["file_write", "file_edit"].includes(toolName)) {
|
|
2219
|
+
// file_write/file_edit/shell_exec만 검증
|
|
2220
|
+
if (!["file_write", "file_edit", "shell_exec"].includes(toolName)) {
|
|
1922
2221
|
return null;
|
|
1923
2222
|
}
|
|
1924
2223
|
const validation = await this.autoFixLoop.validateResult(toolName, result.output, result.success, this.config.loop.projectPath);
|
|
@@ -1953,7 +2252,11 @@ export class AgentLoop extends EventEmitter {
|
|
|
1953
2252
|
parseToolArgs(args) {
|
|
1954
2253
|
if (typeof args === "string") {
|
|
1955
2254
|
try {
|
|
1956
|
-
|
|
2255
|
+
const parsed = JSON.parse(args);
|
|
2256
|
+
if (parsed && typeof parsed === "object") {
|
|
2257
|
+
return parsed;
|
|
2258
|
+
}
|
|
2259
|
+
return { raw: args };
|
|
1957
2260
|
}
|
|
1958
2261
|
catch {
|
|
1959
2262
|
return { raw: args };
|
|
@@ -1973,7 +2276,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
1973
2276
|
// 현재 plan 정보에서 진행 상황 추출
|
|
1974
2277
|
const progress = this.extractProgress();
|
|
1975
2278
|
const checkpoint = {
|
|
1976
|
-
sessionId:
|
|
2279
|
+
sessionId: this.sessionId ?? "unknown-session",
|
|
1977
2280
|
goal: this.contextManager.getMessages().find((m) => m.role === "user")?.content ?? "",
|
|
1978
2281
|
progress,
|
|
1979
2282
|
changedFiles: this.changedFiles.map((path) => ({ path, diff: "" })),
|
|
@@ -1983,7 +2286,9 @@ export class AgentLoop extends EventEmitter {
|
|
|
1983
2286
|
.filter((r) => !r.success)
|
|
1984
2287
|
.slice(-5)
|
|
1985
2288
|
.map((r) => `${r.name}: ${r.output.slice(0, 200)}`),
|
|
1986
|
-
contextUsageAtSave: this.
|
|
2289
|
+
contextUsageAtSave: this.config.loop.totalTokenBudget > 0
|
|
2290
|
+
? this.tokenUsage.total / this.config.loop.totalTokenBudget
|
|
2291
|
+
: 0,
|
|
1987
2292
|
totalTokensUsed: this.tokenUsage.total,
|
|
1988
2293
|
iterationsCompleted: iteration,
|
|
1989
2294
|
createdAt: new Date(),
|
|
@@ -2136,6 +2441,83 @@ export class AgentLoop extends EventEmitter {
|
|
|
2136
2441
|
// Impact analysis 실패는 치명적이지 않음
|
|
2137
2442
|
}
|
|
2138
2443
|
}
|
|
2444
|
+
/**
|
|
2445
|
+
* 다중 파일 변경이 누적된 경우 aggregate impact를 1회만 컨텍스트에 주입한다.
|
|
2446
|
+
* 무거운 분석이므로 complex/massive 태스크에서만 사용.
|
|
2447
|
+
*/
|
|
2448
|
+
async maybeInjectAggregateImpactHint() {
|
|
2449
|
+
if (!this.impactAnalyzer || this.changedFiles.length < 2)
|
|
2450
|
+
return;
|
|
2451
|
+
try {
|
|
2452
|
+
const report = await this.impactAnalyzer.analyzeChanges(this.changedFiles);
|
|
2453
|
+
const shouldInject = report.breakingChanges.length > 0 ||
|
|
2454
|
+
report.deadCodeCandidates.length > 0 ||
|
|
2455
|
+
report.testCoverage.some((t) => t.inferredCoverage === "low") ||
|
|
2456
|
+
report.riskLevel === "high" ||
|
|
2457
|
+
report.riskLevel === "critical";
|
|
2458
|
+
if (!shouldInject)
|
|
2459
|
+
return;
|
|
2460
|
+
const planPreview = report.refactorPlan
|
|
2461
|
+
.slice(0, 3)
|
|
2462
|
+
.map((step) => `${step.step}. ${step.action}`)
|
|
2463
|
+
.join("\n");
|
|
2464
|
+
const hintLines = [
|
|
2465
|
+
"[Aggregate Impact Hint]",
|
|
2466
|
+
`Risk: ${report.riskLevel}`,
|
|
2467
|
+
`Breaking changes: ${report.breakingChanges.length}`,
|
|
2468
|
+
`Dead code candidates: ${report.deadCodeCandidates.length}`,
|
|
2469
|
+
`Low coverage files: ${report.testCoverage.filter((t) => t.inferredCoverage === "low").length}`,
|
|
2470
|
+
];
|
|
2471
|
+
if (planPreview) {
|
|
2472
|
+
hintLines.push("", "Suggested refactor order:", planPreview);
|
|
2473
|
+
}
|
|
2474
|
+
this.contextManager.addMessage({
|
|
2475
|
+
role: "system",
|
|
2476
|
+
content: hintLines.join("\n"),
|
|
2477
|
+
});
|
|
2478
|
+
this.impactHintInjected = true;
|
|
2479
|
+
this.emitEvent({
|
|
2480
|
+
kind: "agent:thinking",
|
|
2481
|
+
content: `Aggregate impact hint injected: ${report.riskLevel} risk, ` +
|
|
2482
|
+
`${report.breakingChanges.length} breaking changes, ` +
|
|
2483
|
+
`${report.deadCodeCandidates.length} dead code candidates.`,
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
catch {
|
|
2487
|
+
// aggregate impact 실패는 치명적이지 않음
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
/**
|
|
2491
|
+
* 종료 직전 최종 impact 요약 생성.
|
|
2492
|
+
* assistant final summary에만 붙이고 system prompt 오염은 하지 않는다.
|
|
2493
|
+
*/
|
|
2494
|
+
async buildFinalImpactSummary() {
|
|
2495
|
+
if (!this.impactAnalyzer || this.changedFiles.length === 0)
|
|
2496
|
+
return null;
|
|
2497
|
+
try {
|
|
2498
|
+
const report = await this.impactAnalyzer.analyzeChanges(this.changedFiles);
|
|
2499
|
+
const lines = [];
|
|
2500
|
+
lines.push("Impact summary:");
|
|
2501
|
+
lines.push(`- Risk: ${report.riskLevel}, affected files: ${report.affectedFiles.length}, breaking changes: ${report.breakingChanges.length}`);
|
|
2502
|
+
const lowCoverage = report.testCoverage.filter((t) => t.inferredCoverage === "low");
|
|
2503
|
+
if (lowCoverage.length > 0) {
|
|
2504
|
+
lines.push(`- Low inferred test coverage: ${lowCoverage.map((t) => t.file).join(", ")}`);
|
|
2505
|
+
}
|
|
2506
|
+
if (report.deadCodeCandidates.length > 0) {
|
|
2507
|
+
lines.push(`- Dead code candidates: ${report.deadCodeCandidates
|
|
2508
|
+
.slice(0, 5)
|
|
2509
|
+
.map((d) => `${d.file}:${d.symbol}`)
|
|
2510
|
+
.join(", ")}`);
|
|
2511
|
+
}
|
|
2512
|
+
if (report.refactorPlan.length > 0) {
|
|
2513
|
+
lines.push(`- Suggested next step: ${report.refactorPlan[0]?.action ?? "review refactor plan"}`);
|
|
2514
|
+
}
|
|
2515
|
+
return lines.join("\n");
|
|
2516
|
+
}
|
|
2517
|
+
catch {
|
|
2518
|
+
return null;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2139
2521
|
/**
|
|
2140
2522
|
* CostOptimizer 인스턴스를 반환한다.
|
|
2141
2523
|
*/
|
|
@@ -2158,6 +2540,25 @@ export class AgentLoop extends EventEmitter {
|
|
|
2158
2540
|
const args = this.parseToolArgs(toolCall.arguments);
|
|
2159
2541
|
return this.mcpClient.callToolAsYuan(toolCall.name, args, toolCall.id);
|
|
2160
2542
|
}
|
|
2543
|
+
/**
|
|
2544
|
+
* ContinuousReflection overflow signal을 soft rollover로 처리한다.
|
|
2545
|
+
* 절대 abort하지 않고, 체크포인트 저장 후 다음 iteration에서
|
|
2546
|
+
* ContextManager가 압축된 컨텍스트를 사용하도록 둔다.
|
|
2547
|
+
*/
|
|
2548
|
+
async handleSoftContextOverflow() {
|
|
2549
|
+
try {
|
|
2550
|
+
await this.saveAutoCheckpoint(this.iterationCount);
|
|
2551
|
+
this.checkpointSaved = true;
|
|
2552
|
+
}
|
|
2553
|
+
catch {
|
|
2554
|
+
// checkpoint 실패는 치명적이지 않음
|
|
2555
|
+
}
|
|
2556
|
+
this.emitEvent({
|
|
2557
|
+
kind: "agent:thinking",
|
|
2558
|
+
content: "Context usage exceeded safe threshold. " +
|
|
2559
|
+
"Saved checkpoint, compacting history, and continuing without abort.",
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2161
2562
|
/** MCP 클라이언트 정리 (세션 종료 시 호출) */
|
|
2162
2563
|
async dispose() {
|
|
2163
2564
|
if (this.mcpClient) {
|
|
@@ -2173,8 +2574,23 @@ export class AgentLoop extends EventEmitter {
|
|
|
2173
2574
|
emitEvent(event) {
|
|
2174
2575
|
this.emit("event", event);
|
|
2175
2576
|
}
|
|
2577
|
+
emitReasoning(content) {
|
|
2578
|
+
this.reasoningTree.add("reasoning", content);
|
|
2579
|
+
this.emitEvent({
|
|
2580
|
+
kind: "agent:reasoning_delta",
|
|
2581
|
+
text: content,
|
|
2582
|
+
source: "agent",
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
emitSubagent(name, phase, content) {
|
|
2586
|
+
this.reasoningTree.add(name, `[${phase}] ${content}`);
|
|
2587
|
+
this.emitReasoning(`[${name}:${phase}] ${content}`);
|
|
2588
|
+
}
|
|
2176
2589
|
handleFatalError(err) {
|
|
2177
2590
|
const message = err instanceof Error ? err.message : String(err);
|
|
2591
|
+
if (this.sessionPersistence && this.sessionId) {
|
|
2592
|
+
void this.sessionPersistence.updateStatus(this.sessionId, "crashed");
|
|
2593
|
+
}
|
|
2178
2594
|
this.emitEvent({
|
|
2179
2595
|
kind: "agent:error",
|
|
2180
2596
|
message,
|