@yuaone/core 0.3.2 → 0.4.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 +62 -0
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +705 -18
- package/dist/agent-loop.js.map +1 -1
- package/dist/background-agent.d.ts +110 -0
- package/dist/background-agent.d.ts.map +1 -0
- package/dist/background-agent.js +255 -0
- package/dist/background-agent.js.map +1 -0
- package/dist/coding-standards.d.ts +45 -0
- package/dist/coding-standards.d.ts.map +1 -0
- package/dist/coding-standards.js +1152 -0
- package/dist/coding-standards.js.map +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -6
- package/dist/constants.js.map +1 -1
- package/dist/context-manager.d.ts +6 -0
- package/dist/context-manager.d.ts.map +1 -1
- package/dist/context-manager.js +23 -4
- package/dist/context-manager.js.map +1 -1
- package/dist/index.d.ts +28 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -2
- package/dist/index.js.map +1 -1
- package/dist/llm-client.d.ts +8 -3
- package/dist/llm-client.d.ts.map +1 -1
- package/dist/llm-client.js +64 -13
- package/dist/llm-client.js.map +1 -1
- package/dist/plugin-auto-loader.d.ts +108 -0
- package/dist/plugin-auto-loader.d.ts.map +1 -0
- package/dist/plugin-auto-loader.js +743 -0
- package/dist/plugin-auto-loader.js.map +1 -0
- package/dist/plugin-registry.d.ts +112 -0
- package/dist/plugin-registry.d.ts.map +1 -0
- package/dist/plugin-registry.js +319 -0
- package/dist/plugin-registry.js.map +1 -0
- package/dist/plugin-types.d.ts +388 -0
- package/dist/plugin-types.d.ts.map +1 -0
- package/dist/plugin-types.js +8 -0
- package/dist/plugin-types.js.map +1 -0
- package/dist/plugin-validator.d.ts +54 -0
- package/dist/plugin-validator.d.ts.map +1 -0
- package/dist/plugin-validator.js +129 -0
- package/dist/plugin-validator.js.map +1 -0
- package/dist/repo-knowledge-graph.d.ts +112 -0
- package/dist/repo-knowledge-graph.d.ts.map +1 -0
- package/dist/repo-knowledge-graph.js +561 -0
- package/dist/repo-knowledge-graph.js.map +1 -0
- package/dist/role-registry.js +1 -1
- package/dist/role-registry.js.map +1 -1
- package/dist/self-debug-loop.d.ts +257 -0
- package/dist/self-debug-loop.d.ts.map +1 -0
- package/dist/self-debug-loop.js +870 -0
- package/dist/self-debug-loop.js.map +1 -0
- package/dist/skill-learner.d.ts +136 -0
- package/dist/skill-learner.d.ts.map +1 -0
- package/dist/skill-learner.js +382 -0
- package/dist/skill-learner.js.map +1 -0
- package/dist/skill-loader.d.ts +90 -0
- package/dist/skill-loader.d.ts.map +1 -0
- package/dist/skill-loader.js +309 -0
- package/dist/skill-loader.js.map +1 -0
- package/dist/specialist-registry.d.ts +132 -0
- package/dist/specialist-registry.d.ts.map +1 -0
- package/dist/specialist-registry.js +413 -0
- package/dist/specialist-registry.js.map +1 -0
- package/dist/sub-agent-prompts.d.ts +45 -0
- package/dist/sub-agent-prompts.d.ts.map +1 -0
- package/dist/sub-agent-prompts.js +177 -0
- package/dist/sub-agent-prompts.js.map +1 -0
- package/dist/sub-agent-router.d.ts +75 -0
- package/dist/sub-agent-router.d.ts.map +1 -0
- package/dist/sub-agent-router.js +174 -0
- package/dist/sub-agent-router.js.map +1 -0
- package/dist/sub-agent.d.ts +48 -0
- package/dist/sub-agent.d.ts.map +1 -1
- package/dist/sub-agent.js +108 -5
- package/dist/sub-agent.js.map +1 -1
- package/dist/system-prompt.d.ts +26 -0
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +177 -7
- package/dist/system-prompt.js.map +1 -1
- package/dist/task-classifier.d.ts +25 -1
- package/dist/task-classifier.d.ts.map +1 -1
- package/dist/task-classifier.js +171 -1
- package/dist/task-classifier.js.map +1 -1
- package/dist/tool-planner.d.ts +160 -0
- package/dist/tool-planner.d.ts.map +1 -0
- package/dist/tool-planner.js +501 -0
- package/dist/tool-planner.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/world-state.d.ts.map +1 -1
- package/dist/world-state.js +8 -1
- package/dist/world-state.js.map +1 -1
- package/package.json +2 -1
- package/plugins/git/patterns/branch-patterns.json +101 -0
- package/plugins/git/patterns/commit-patterns.json +186 -0
- package/plugins/git/plugin.yaml +128 -0
- package/plugins/git/skills/branch-strategy.md +172 -0
- package/plugins/git/skills/commit-conv.md +178 -0
- package/plugins/git/skills/conflict-resolve.md +159 -0
- package/plugins/git/skills/history-clean.md +199 -0
- package/plugins/git/skills/pr-review.md +196 -0
- package/plugins/git/strategies/conflict-resolve.json +244 -0
- package/plugins/git/strategies/release-flow.json +292 -0
- package/plugins/git/validators/rules.json +348 -0
- package/plugins/react/patterns/anti-patterns.json +88 -0
- package/plugins/react/patterns/components.json +80 -0
- package/plugins/react/patterns/hooks.json +72 -0
- package/plugins/react/plugin.yaml +229 -0
- package/plugins/react/skills/bugfix.md +208 -0
- package/plugins/react/skills/component-gen.md +206 -0
- package/plugins/react/skills/hook-extract.md +208 -0
- package/plugins/react/skills/ssr.md +256 -0
- package/plugins/react/skills/test.md +273 -0
- package/plugins/react/strategies/build-fix.json +43 -0
- package/plugins/react/strategies/hook-loop-fix.json +36 -0
- package/plugins/react/strategies/hydration-fix.json +42 -0
- package/plugins/react/validators/rules.json +92 -0
- package/plugins/typescript/patterns/best-practices.json +25 -0
- package/plugins/typescript/patterns/common-errors.json +32 -0
- package/plugins/typescript/plugin.yaml +74 -0
- package/plugins/typescript/skills/debug.md +23 -0
- package/plugins/typescript/skills/migration.md +24 -0
- package/plugins/typescript/skills/refactor.md +22 -0
- package/plugins/typescript/skills/strict-mode.md +23 -0
- package/plugins/typescript/strategies/strict-migration.json +37 -0
- package/plugins/typescript/strategies/type-error-fix.json +37 -0
- package/plugins/typescript/validators/rules.json +28 -0
package/dist/agent-loop.js
CHANGED
|
@@ -30,6 +30,17 @@ import { FailureRecovery } from "./failure-recovery.js";
|
|
|
30
30
|
import { ExecutionPolicyEngine } from "./execution-policy-engine.js";
|
|
31
31
|
import { CostOptimizer } from "./cost-optimizer.js";
|
|
32
32
|
import { ImpactAnalyzer } from "./impact-analyzer.js";
|
|
33
|
+
import { SelfReflection } from "./self-reflection.js";
|
|
34
|
+
import { DebateOrchestrator } from "./debate-orchestrator.js";
|
|
35
|
+
import { ContinuousReflection, } from "./continuous-reflection.js";
|
|
36
|
+
import { PluginRegistry } from "./plugin-registry.js";
|
|
37
|
+
import { SkillLoader } from "./skill-loader.js";
|
|
38
|
+
import { SpecialistRegistry } from "./specialist-registry.js";
|
|
39
|
+
import { ToolPlanner } from "./tool-planner.js";
|
|
40
|
+
import { SelfDebugLoop } from "./self-debug-loop.js";
|
|
41
|
+
import { SkillLearner } from "./skill-learner.js";
|
|
42
|
+
import { RepoKnowledgeGraph } from "./repo-knowledge-graph.js";
|
|
43
|
+
import { BackgroundAgentManager } from "./background-agent.js";
|
|
33
44
|
/**
|
|
34
45
|
* AgentLoop — YUAN 에이전트의 핵심 실행 루프.
|
|
35
46
|
*
|
|
@@ -93,11 +104,35 @@ export class AgentLoop extends EventEmitter {
|
|
|
93
104
|
worldState = null;
|
|
94
105
|
costOptimizer;
|
|
95
106
|
impactAnalyzer = null;
|
|
107
|
+
selfReflection = null;
|
|
108
|
+
debateOrchestrator = null;
|
|
109
|
+
continuousReflection = null;
|
|
110
|
+
enableSelfReflection;
|
|
111
|
+
enableDebate;
|
|
112
|
+
currentComplexity = "simple";
|
|
96
113
|
policyOverrides;
|
|
97
114
|
checkpointSaved = false;
|
|
98
115
|
iterationCount = 0;
|
|
99
116
|
originalSnapshots = new Map();
|
|
100
117
|
previousStrategies = [];
|
|
118
|
+
pluginRegistry;
|
|
119
|
+
skillLoader;
|
|
120
|
+
specialistRegistry;
|
|
121
|
+
toolPlanner;
|
|
122
|
+
selfDebugLoop;
|
|
123
|
+
skillLearner = null;
|
|
124
|
+
repoGraph = null;
|
|
125
|
+
backgroundAgentManager = null;
|
|
126
|
+
enableToolPlanning;
|
|
127
|
+
enableSkillLearning;
|
|
128
|
+
enableBackgroundAgents;
|
|
129
|
+
currentToolPlan = null;
|
|
130
|
+
executedToolNames = [];
|
|
131
|
+
/** Context Budget: max 3 active skills at once */
|
|
132
|
+
activeSkillIds = [];
|
|
133
|
+
static MAX_ACTIVE_SKILLS = 3;
|
|
134
|
+
/** Context Budget: track injected system messages per iteration to cap at 5 */
|
|
135
|
+
iterationSystemMsgCount = 0;
|
|
101
136
|
tokenUsage = {
|
|
102
137
|
input: 0,
|
|
103
138
|
output: 0,
|
|
@@ -112,6 +147,8 @@ export class AgentLoop extends EventEmitter {
|
|
|
112
147
|
this.enablePlanning = options.enablePlanning !== false;
|
|
113
148
|
this.planningThreshold = options.planningThreshold ?? "moderate";
|
|
114
149
|
this.environment = options.environment;
|
|
150
|
+
this.enableSelfReflection = options.enableSelfReflection !== false;
|
|
151
|
+
this.enableDebate = options.enableDebate !== false;
|
|
115
152
|
// BYOK LLM 클라이언트 생성
|
|
116
153
|
this.llmClient = new BYOKClient(options.config.byok);
|
|
117
154
|
// Governor 생성
|
|
@@ -145,6 +182,16 @@ export class AgentLoop extends EventEmitter {
|
|
|
145
182
|
// InterruptManager 설정 (외부 주입 또는 내부 생성)
|
|
146
183
|
this.interruptManager = options.interruptManager ?? new InterruptManager();
|
|
147
184
|
this.setupInterruptListeners();
|
|
185
|
+
// PluginRegistry + SkillLoader 초기화
|
|
186
|
+
this.pluginRegistry = options.pluginRegistry ?? new PluginRegistry();
|
|
187
|
+
this.skillLoader = new SkillLoader();
|
|
188
|
+
// Advanced Intelligence 모듈 초기화
|
|
189
|
+
this.specialistRegistry = new SpecialistRegistry();
|
|
190
|
+
this.toolPlanner = new ToolPlanner();
|
|
191
|
+
this.selfDebugLoop = new SelfDebugLoop();
|
|
192
|
+
this.enableToolPlanning = options.enableToolPlanning !== false;
|
|
193
|
+
this.enableSkillLearning = options.enableSkillLearning !== false;
|
|
194
|
+
this.enableBackgroundAgents = options.enableBackgroundAgents === true;
|
|
148
195
|
// 시스템 프롬프트 추가 (메모리 없이 기본 프롬프트로 시작, init()에서 갱신)
|
|
149
196
|
this.contextManager.addMessage({
|
|
150
197
|
role: "system",
|
|
@@ -260,6 +307,40 @@ export class AgentLoop extends EventEmitter {
|
|
|
260
307
|
if (projectPath) {
|
|
261
308
|
this.reflexionEngine = new ReflexionEngine({ projectPath });
|
|
262
309
|
}
|
|
310
|
+
// SelfReflection 생성 (6D deep verify + quick verify)
|
|
311
|
+
if (this.enableSelfReflection) {
|
|
312
|
+
const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
313
|
+
this.selfReflection = new SelfReflection(sessionId, {
|
|
314
|
+
enableDeepVerify: true,
|
|
315
|
+
enableMonologue: true,
|
|
316
|
+
enableLearning: true,
|
|
317
|
+
minScoreToPass: 70,
|
|
318
|
+
criticalDimensions: ["correctness", "security"],
|
|
319
|
+
});
|
|
320
|
+
// 메모리에서 기존 학습 복원
|
|
321
|
+
if (this.memoryManager) {
|
|
322
|
+
try {
|
|
323
|
+
const memory = await this.memoryManager.load();
|
|
324
|
+
this.selfReflection.loadFromMemory(memory);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// 학습 복원 실패는 치명적이지 않음
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// DebateOrchestrator 생성 (complex/massive 태스크에서 multi-agent debate)
|
|
332
|
+
if (this.enableDebate && projectPath) {
|
|
333
|
+
this.debateOrchestrator = new DebateOrchestrator({
|
|
334
|
+
projectPath,
|
|
335
|
+
maxRounds: 3,
|
|
336
|
+
qualityThreshold: 80,
|
|
337
|
+
verifyBetweenRounds: true,
|
|
338
|
+
byokConfig: this.config.byok,
|
|
339
|
+
toolExecutor: this.toolExecutor,
|
|
340
|
+
maxTokensPerCall: 16384,
|
|
341
|
+
totalTokenBudget: Math.floor(this.config.loop.totalTokenBudget * 0.3),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
263
344
|
// 향상된 시스템 프롬프트 생성
|
|
264
345
|
const enhancedPrompt = buildSystemPrompt({
|
|
265
346
|
projectStructure,
|
|
@@ -289,6 +370,152 @@ export class AgentLoop extends EventEmitter {
|
|
|
289
370
|
}
|
|
290
371
|
}
|
|
291
372
|
}
|
|
373
|
+
// SkillLearner 초기화 (경험에서 학습된 스킬 자동 로드)
|
|
374
|
+
if (this.enableSkillLearning && projectPath) {
|
|
375
|
+
try {
|
|
376
|
+
this.skillLearner = new SkillLearner(projectPath);
|
|
377
|
+
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
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
// SkillLearner 초기화 실패는 치명적이지 않음
|
|
394
|
+
this.skillLearner = null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// RepoKnowledgeGraph 초기화 (코드 구조 그래프 — 비동기 빌드)
|
|
398
|
+
if (projectPath) {
|
|
399
|
+
try {
|
|
400
|
+
this.repoGraph = new RepoKnowledgeGraph(projectPath);
|
|
401
|
+
// 백그라운드에서 그래프 빌드 (블로킹하지 않음)
|
|
402
|
+
this.repoGraph.buildFromProject(projectPath).catch(() => {
|
|
403
|
+
// 그래프 빌드 실패는 치명적이지 않음
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
this.repoGraph = null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// BackgroundAgentManager 초기화 (opt-in)
|
|
411
|
+
if (this.enableBackgroundAgents && projectPath) {
|
|
412
|
+
try {
|
|
413
|
+
this.backgroundAgentManager = new BackgroundAgentManager();
|
|
414
|
+
this.backgroundAgentManager.createDefaults(projectPath);
|
|
415
|
+
// Background events → agent loop events
|
|
416
|
+
for (const agent of this.backgroundAgentManager.list()) {
|
|
417
|
+
const bgAgent = this.backgroundAgentManager.get(agent.id);
|
|
418
|
+
if (bgAgent) {
|
|
419
|
+
bgAgent.on("event", (event) => {
|
|
420
|
+
if (event.type === "error" || event.type === "warning") {
|
|
421
|
+
this.emitEvent({
|
|
422
|
+
kind: "agent:thinking",
|
|
423
|
+
content: `[Background: ${event.agentId}] ${event.message}`,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
this.backgroundAgentManager = null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// Inject active plugin skills into system prompt (lazy: names only)
|
|
435
|
+
// Full skill content is loaded on-demand when triggered (file-pattern or error match)
|
|
436
|
+
const activeSkills = this.pluginRegistry.getAllSkills();
|
|
437
|
+
if (activeSkills.length > 0) {
|
|
438
|
+
const skillSummary = activeSkills
|
|
439
|
+
.map((s) => `${s.pluginId}/${s.skill.name}`)
|
|
440
|
+
.join(", ");
|
|
441
|
+
this.contextManager.addMessage({
|
|
442
|
+
role: "system",
|
|
443
|
+
content: `[Plugins: ${skillSummary}] — Skills auto-activate on matching files/errors.`,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
// ContinuousReflection 생성 (1분 간격 체크포인트 + 자기검증 + 컨텍스트 모니터)
|
|
447
|
+
this.continuousReflection = new ContinuousReflection({
|
|
448
|
+
getState: () => this.getStateSnapshot(),
|
|
449
|
+
checkpoint: async (state, emergency) => {
|
|
450
|
+
if (!this.continuationEngine)
|
|
451
|
+
return;
|
|
452
|
+
const checkpoint = {
|
|
453
|
+
sessionId: crypto.randomUUID(),
|
|
454
|
+
goal: state.goal,
|
|
455
|
+
progress: {
|
|
456
|
+
completedTasks: state.completedTasks,
|
|
457
|
+
currentTask: state.currentTask,
|
|
458
|
+
remainingTasks: state.remainingTasks,
|
|
459
|
+
},
|
|
460
|
+
changedFiles: state.changedFiles.map((path) => ({ path, diff: "" })),
|
|
461
|
+
workingMemory: state.workingMemory,
|
|
462
|
+
yuanMdUpdates: [],
|
|
463
|
+
errors: state.errors,
|
|
464
|
+
contextUsageAtSave: state.contextUsagePercent,
|
|
465
|
+
totalTokensUsed: state.totalTokensUsed,
|
|
466
|
+
iterationsCompleted: state.iteration,
|
|
467
|
+
createdAt: new Date(),
|
|
468
|
+
};
|
|
469
|
+
await this.continuationEngine.saveCheckpoint(checkpoint);
|
|
470
|
+
},
|
|
471
|
+
selfVerify: async (prompt) => {
|
|
472
|
+
// 경량 LLM 호출 (~200 토큰)로 자기검증
|
|
473
|
+
try {
|
|
474
|
+
const response = await this.llmClient.chat([{ role: "user", content: prompt }], []);
|
|
475
|
+
const text = typeof response.content === "string"
|
|
476
|
+
? response.content
|
|
477
|
+
: "";
|
|
478
|
+
const parsed = JSON.parse(text);
|
|
479
|
+
return {
|
|
480
|
+
onTrack: Boolean(parsed.onTrack),
|
|
481
|
+
needsCorrection: Boolean(parsed.needsCorrection),
|
|
482
|
+
issue: String(parsed.issue ?? ""),
|
|
483
|
+
suggestion: String(parsed.suggestion ?? ""),
|
|
484
|
+
confidence: Number(parsed.confidence ?? 0.5),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
return {
|
|
489
|
+
onTrack: true,
|
|
490
|
+
needsCorrection: false,
|
|
491
|
+
issue: "",
|
|
492
|
+
suggestion: "",
|
|
493
|
+
confidence: 0.5,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
// Wire ContinuousReflection events to agent loop
|
|
499
|
+
this.continuousReflection.on("reflection:feedback", (feedback) => {
|
|
500
|
+
this.contextManager.addMessage({
|
|
501
|
+
role: "system",
|
|
502
|
+
content: `[ContinuousReflection] ${feedback}`,
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
this.continuousReflection.on("reflection:context_warning", (usagePercent) => {
|
|
506
|
+
this.emitEvent({
|
|
507
|
+
kind: "agent:thinking",
|
|
508
|
+
content: `Context usage at ${Math.round(usagePercent * 100)}% — emergency checkpoint saved.`,
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
this.continuousReflection.on("reflection:context_overflow", () => {
|
|
512
|
+
// Trigger abort — the continuation checkpoint is already saved
|
|
513
|
+
this.emitEvent({
|
|
514
|
+
kind: "agent:thinking",
|
|
515
|
+
content: "Context usage at 95%+ — saving state and stopping.",
|
|
516
|
+
});
|
|
517
|
+
this.abort();
|
|
518
|
+
});
|
|
292
519
|
}
|
|
293
520
|
/**
|
|
294
521
|
* MemoryManager의 학습/실패 기록을 시스템 메시지로 변환.
|
|
@@ -333,6 +560,8 @@ export class AgentLoop extends EventEmitter {
|
|
|
333
560
|
this.iterationCount = 0;
|
|
334
561
|
this.originalSnapshots.clear();
|
|
335
562
|
this.previousStrategies = [];
|
|
563
|
+
this.activeSkillIds = [];
|
|
564
|
+
this.iterationSystemMsgCount = 0;
|
|
336
565
|
this.failureRecovery.reset();
|
|
337
566
|
this.costOptimizer.reset();
|
|
338
567
|
this.tokenBudgetManager.reset();
|
|
@@ -383,11 +612,94 @@ export class AgentLoop extends EventEmitter {
|
|
|
383
612
|
content: classificationHint,
|
|
384
613
|
});
|
|
385
614
|
}
|
|
615
|
+
// Specialist routing: 태스크 타입에 맞는 전문 에이전트 설정 주입
|
|
616
|
+
if (classification.specialistDomain) {
|
|
617
|
+
const specialistMatch = this.specialistRegistry.findSpecialist(classification.specialistDomain);
|
|
618
|
+
if (specialistMatch && specialistMatch.confidence >= 0.5) {
|
|
619
|
+
this.contextManager.addMessage({
|
|
620
|
+
role: "system",
|
|
621
|
+
content: `[Specialist: ${specialistMatch.specialist.name}] ${specialistMatch.specialist.systemPrompt.slice(0, 500)}`,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
// Tool Planning: 태스크 타입에 맞는 도구 실행 계획 힌트 주입
|
|
626
|
+
if (this.enableToolPlanning && classification.confidence >= 0.3) {
|
|
627
|
+
const planContext = {
|
|
628
|
+
userMessage,
|
|
629
|
+
};
|
|
630
|
+
this.currentToolPlan = this.toolPlanner.planForTask(classification.type, planContext);
|
|
631
|
+
this.executedToolNames = [];
|
|
632
|
+
if (this.currentToolPlan.confidence >= 0.5) {
|
|
633
|
+
const planHint = this.toolPlanner.formatPlanHint(this.currentToolPlan);
|
|
634
|
+
this.contextManager.addMessage({
|
|
635
|
+
role: "system",
|
|
636
|
+
content: planHint,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
386
640
|
// 복잡도 감지 → 필요 시 자동 플래닝
|
|
387
641
|
await this.maybeCreatePlan(userMessage);
|
|
388
|
-
|
|
642
|
+
// ContinuousReflection 시작 (1분 간격 체크포인트/자기검증/컨텍스트모니터)
|
|
643
|
+
if (this.continuousReflection) {
|
|
644
|
+
this.continuousReflection.start();
|
|
645
|
+
}
|
|
646
|
+
let result;
|
|
647
|
+
try {
|
|
648
|
+
result = await this.executeLoop();
|
|
649
|
+
}
|
|
650
|
+
finally {
|
|
651
|
+
// ContinuousReflection 정지 (루프 종료 시 반드시 정리)
|
|
652
|
+
if (this.continuousReflection) {
|
|
653
|
+
this.continuousReflection.stop();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
389
656
|
// 실행 완료 후 메모리 자동 업데이트
|
|
390
657
|
await this.updateMemoryAfterRun(userMessage, result, Date.now() - runStartTime);
|
|
658
|
+
// SkillLearner: 성공적 에러 해결 시 새로운 스킬 학습
|
|
659
|
+
if (this.skillLearner && result.reason === "GOAL_ACHIEVED") {
|
|
660
|
+
try {
|
|
661
|
+
const errorToolResults = this.allToolResults.filter((r) => !r.success);
|
|
662
|
+
if (errorToolResults.length > 0 && this.changedFiles.length > 0) {
|
|
663
|
+
// 에러가 있었지만 결국 성공 → 학습 가능한 패턴
|
|
664
|
+
const runAnalysis = this.memoryUpdater.analyzeRun({
|
|
665
|
+
goal: userMessage,
|
|
666
|
+
termination: { reason: result.reason, summary: result.summary },
|
|
667
|
+
toolResults: this.allToolResults.map((r) => ({
|
|
668
|
+
name: r.name,
|
|
669
|
+
success: r.success,
|
|
670
|
+
output: r.output.slice(0, 500),
|
|
671
|
+
})),
|
|
672
|
+
changedFiles: this.changedFiles,
|
|
673
|
+
messages: [],
|
|
674
|
+
tokensUsed: this.tokenUsage.total,
|
|
675
|
+
durationMs: Date.now() - runStartTime,
|
|
676
|
+
iterations: this.iterationCount,
|
|
677
|
+
});
|
|
678
|
+
this.skillLearner.extractSkillFromRun(runAnalysis, `session-${Date.now()}`);
|
|
679
|
+
await this.skillLearner.save();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
catch {
|
|
683
|
+
// 학습 실패는 치명적이지 않음
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// Tool plan compliance check (non-blocking, for metrics)
|
|
687
|
+
if (this.currentToolPlan && this.executedToolNames.length > 0) {
|
|
688
|
+
try {
|
|
689
|
+
this.toolPlanner.validateExecution(this.currentToolPlan, this.executedToolNames);
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
// compliance check 실패는 치명적이지 않음
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
// RepoKnowledgeGraph: 변경 파일 그래프 업데이트
|
|
696
|
+
if (this.repoGraph && this.changedFiles.length > 0) {
|
|
697
|
+
this.repoGraph.updateFiles(this.changedFiles).catch(() => {
|
|
698
|
+
// 그래프 업데이트 실패는 치명적이지 않음
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
// BackgroundAgentManager: 정리 (stopAll은 abort 시에만)
|
|
702
|
+
// Background agents는 세션 간 지속되므로 여기서 stop하지 않음
|
|
391
703
|
return result;
|
|
392
704
|
}
|
|
393
705
|
catch (err) {
|
|
@@ -504,6 +816,68 @@ export class AgentLoop extends EventEmitter {
|
|
|
504
816
|
getTokenUsage() {
|
|
505
817
|
return { ...this.tokenUsage };
|
|
506
818
|
}
|
|
819
|
+
/** Get the plugin registry for external management */
|
|
820
|
+
getPluginRegistry() {
|
|
821
|
+
return this.pluginRegistry;
|
|
822
|
+
}
|
|
823
|
+
/** Get the specialist registry for custom specialist registration */
|
|
824
|
+
getSpecialistRegistry() {
|
|
825
|
+
return this.specialistRegistry;
|
|
826
|
+
}
|
|
827
|
+
/** Get the skill learner for inspection */
|
|
828
|
+
getSkillLearner() {
|
|
829
|
+
return this.skillLearner;
|
|
830
|
+
}
|
|
831
|
+
/** Get the repo knowledge graph */
|
|
832
|
+
getRepoGraph() {
|
|
833
|
+
return this.repoGraph;
|
|
834
|
+
}
|
|
835
|
+
/** Get background agent manager */
|
|
836
|
+
getBackgroundAgentManager() {
|
|
837
|
+
return this.backgroundAgentManager;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* ContinuousReflection이 사용하는 에이전트 상태 스냅샷을 생성한다.
|
|
841
|
+
* 읽기 전용 — reflection tick마다 호출된다.
|
|
842
|
+
*/
|
|
843
|
+
getStateSnapshot() {
|
|
844
|
+
const userMsg = this.contextManager.getMessages().find((m) => m.role === "user");
|
|
845
|
+
const goal = typeof userMsg?.content === "string" ? userMsg.content : "";
|
|
846
|
+
const progress = this.extractProgress();
|
|
847
|
+
const recentTools = this.allToolResults.slice(-3).map((r) => ({
|
|
848
|
+
tool: r.name,
|
|
849
|
+
input: typeof r.tool_call_id === "string" ? r.tool_call_id : "",
|
|
850
|
+
output: r.output.slice(0, 200),
|
|
851
|
+
}));
|
|
852
|
+
const errors = this.allToolResults
|
|
853
|
+
.filter((r) => !r.success)
|
|
854
|
+
.slice(-5)
|
|
855
|
+
.map((r) => `${r.name}: ${r.output.slice(0, 200)}`);
|
|
856
|
+
return {
|
|
857
|
+
goal,
|
|
858
|
+
iteration: this.iterationCount,
|
|
859
|
+
maxIteration: this.config.loop.maxIterations,
|
|
860
|
+
changedFiles: [...this.changedFiles],
|
|
861
|
+
recentToolCalls: recentTools,
|
|
862
|
+
errors,
|
|
863
|
+
contextUsagePercent: this.config.loop.totalTokenBudget > 0
|
|
864
|
+
? this.tokenUsage.total / this.config.loop.totalTokenBudget
|
|
865
|
+
: 0,
|
|
866
|
+
sessionId: crypto.randomUUID(),
|
|
867
|
+
totalTokensUsed: this.tokenUsage.total,
|
|
868
|
+
workingMemory: this.buildWorkingMemorySummary(),
|
|
869
|
+
completedTasks: progress.completedTasks,
|
|
870
|
+
currentTask: progress.currentTask,
|
|
871
|
+
remainingTasks: progress.remainingTasks,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* ContinuousReflection 인스턴스를 반환한다.
|
|
876
|
+
* 외부에서 ESC 토글(pause/resume) 또는 이벤트 구독에 사용.
|
|
877
|
+
*/
|
|
878
|
+
getContinuousReflection() {
|
|
879
|
+
return this.continuousReflection;
|
|
880
|
+
}
|
|
507
881
|
/**
|
|
508
882
|
* 대화 히스토리를 반환.
|
|
509
883
|
*/
|
|
@@ -544,6 +918,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
544
918
|
if (!this.planner || !this.enablePlanning)
|
|
545
919
|
return;
|
|
546
920
|
const complexity = this.detectComplexity(userMessage);
|
|
921
|
+
this.currentComplexity = complexity;
|
|
547
922
|
// 임계값 미만이면 플래닝 스킵
|
|
548
923
|
const thresholdOrder = { simple: 1, moderate: 2, complex: 3 };
|
|
549
924
|
const complexityOrder = {
|
|
@@ -771,6 +1146,36 @@ export class AgentLoop extends EventEmitter {
|
|
|
771
1146
|
iteration++;
|
|
772
1147
|
this.iterationCount = iteration;
|
|
773
1148
|
const iterationStart = Date.now();
|
|
1149
|
+
this.iterationSystemMsgCount = 0; // Reset per-iteration system message counter
|
|
1150
|
+
// Check file-pattern skill triggers based on changed files
|
|
1151
|
+
// Guard: skip skill injection if over 80% token budget to preserve remaining budget
|
|
1152
|
+
// Guard: max 3 active skills globally (Context Budget Rules)
|
|
1153
|
+
const fileTriggerBudgetRatio = this.config.loop.totalTokenBudget > 0
|
|
1154
|
+
? this.tokenUsage.total / this.config.loop.totalTokenBudget
|
|
1155
|
+
: 0;
|
|
1156
|
+
if (this.changedFiles.length > 0 &&
|
|
1157
|
+
fileTriggerBudgetRatio <= 0.8 &&
|
|
1158
|
+
this.activeSkillIds.length < AgentLoop.MAX_ACTIVE_SKILLS) {
|
|
1159
|
+
const lastFile = this.changedFiles[this.changedFiles.length - 1];
|
|
1160
|
+
const fileSkills = this.pluginRegistry.findMatchingSkills({
|
|
1161
|
+
filePath: lastFile,
|
|
1162
|
+
});
|
|
1163
|
+
// Max 2 skills per iteration, capped by global 3-skill limit
|
|
1164
|
+
const slotsRemaining = AgentLoop.MAX_ACTIVE_SKILLS - this.activeSkillIds.length;
|
|
1165
|
+
for (const skill of fileSkills.slice(0, Math.min(2, slotsRemaining))) {
|
|
1166
|
+
if (this.activeSkillIds.includes(skill.id))
|
|
1167
|
+
continue; // no duplicate
|
|
1168
|
+
const parsed = this.skillLoader.loadTemplate(skill);
|
|
1169
|
+
if (parsed) {
|
|
1170
|
+
this.contextManager.addMessage({
|
|
1171
|
+
role: "system",
|
|
1172
|
+
content: `[File Skill: ${skill.name}] ${parsed.domain ? `[${parsed.domain}] ` : ""}${skill.description}`,
|
|
1173
|
+
});
|
|
1174
|
+
this.activeSkillIds.push(skill.id);
|
|
1175
|
+
this.iterationSystemMsgCount++;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
774
1179
|
// 1. 컨텍스트 준비
|
|
775
1180
|
const messages = this.contextManager.prepareForLLM();
|
|
776
1181
|
// 2. LLM 호출 (streaming)
|
|
@@ -832,6 +1237,106 @@ export class AgentLoop extends EventEmitter {
|
|
|
832
1237
|
// 3. 응답 처리
|
|
833
1238
|
if (response.toolCalls.length === 0) {
|
|
834
1239
|
const content = response.content ?? "";
|
|
1240
|
+
// Level 2: Deep verification before declaring completion
|
|
1241
|
+
if (this.selfReflection && this.changedFiles.length > 0) {
|
|
1242
|
+
try {
|
|
1243
|
+
const changedFilesMap = new Map();
|
|
1244
|
+
for (const filePath of this.changedFiles) {
|
|
1245
|
+
const lastWrite = this.allToolResults
|
|
1246
|
+
.filter((r) => r.name === "file_write" || r.name === "file_edit")
|
|
1247
|
+
.find((r) => r.output.includes(filePath));
|
|
1248
|
+
if (lastWrite) {
|
|
1249
|
+
changedFilesMap.set(filePath, lastWrite.output);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
const verifyFn = async (prompt) => {
|
|
1253
|
+
const verifyResponse = await this.llmClient.chat([
|
|
1254
|
+
{ role: "system", content: "You are a meticulous code reviewer." },
|
|
1255
|
+
{ role: "user", content: prompt },
|
|
1256
|
+
]);
|
|
1257
|
+
this.tokenBudgetManager.recordUsage("validator", verifyResponse.usage.input, verifyResponse.usage.output);
|
|
1258
|
+
return verifyResponse.content ?? "";
|
|
1259
|
+
};
|
|
1260
|
+
const deepResult = await this.selfReflection.deepVerify(content || "Complete the task", changedFilesMap, this.originalSnapshots, [], // projectPatterns — could be enhanced with YUAN.md rules
|
|
1261
|
+
verifyFn);
|
|
1262
|
+
if (deepResult.verdict === "fail" && deepResult.confidence >= 0.5) {
|
|
1263
|
+
// Not confident enough — inject feedback and continue loop
|
|
1264
|
+
const issuesList = deepResult.suggestedFixes
|
|
1265
|
+
.map((f) => `[${f.severity}] ${f.description}`)
|
|
1266
|
+
.join("; ");
|
|
1267
|
+
this.contextManager.addMessage({
|
|
1268
|
+
role: "assistant",
|
|
1269
|
+
content: content || "",
|
|
1270
|
+
});
|
|
1271
|
+
this.contextManager.addMessage({
|
|
1272
|
+
role: "system",
|
|
1273
|
+
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
|
+
});
|
|
1275
|
+
this.emitEvent({
|
|
1276
|
+
kind: "agent:thinking",
|
|
1277
|
+
content: `Deep verification failed (score: ${deepResult.overallScore}). Continuing to address issues...`,
|
|
1278
|
+
});
|
|
1279
|
+
continue; // Don't return GOAL_ACHIEVED, continue the loop
|
|
1280
|
+
}
|
|
1281
|
+
// Level 3: Multi-agent debate for complex/massive tasks
|
|
1282
|
+
if (this.debateOrchestrator &&
|
|
1283
|
+
["complex", "massive"].includes(this.currentComplexity) &&
|
|
1284
|
+
deepResult.verdict !== "pass") {
|
|
1285
|
+
try {
|
|
1286
|
+
this.emitEvent({
|
|
1287
|
+
kind: "agent:thinking",
|
|
1288
|
+
content: `Triggering multi-agent debate for ${this.currentComplexity} task verification...`,
|
|
1289
|
+
});
|
|
1290
|
+
const debateContext = [
|
|
1291
|
+
`Changed files: ${this.changedFiles.join(", ")}`,
|
|
1292
|
+
`Self-reflection score: ${deepResult.overallScore}/100`,
|
|
1293
|
+
`Concerns: ${deepResult.selfCritique}`,
|
|
1294
|
+
].join("\n");
|
|
1295
|
+
const debateResult = await this.debateOrchestrator.debate(content || "Verify task completion", debateContext);
|
|
1296
|
+
// Track debate token usage (split roughly 60/40 input/output)
|
|
1297
|
+
const debateInput = Math.round(debateResult.totalTokensUsed * 0.6);
|
|
1298
|
+
const debateOutput = debateResult.totalTokensUsed - debateInput;
|
|
1299
|
+
this.tokenUsage.input += debateInput;
|
|
1300
|
+
this.tokenUsage.output += debateOutput;
|
|
1301
|
+
this.tokenUsage.total += debateResult.totalTokensUsed;
|
|
1302
|
+
if (!debateResult.success) {
|
|
1303
|
+
this.contextManager.addMessage({
|
|
1304
|
+
role: "assistant",
|
|
1305
|
+
content: content || "",
|
|
1306
|
+
});
|
|
1307
|
+
this.contextManager.addMessage({
|
|
1308
|
+
role: "system",
|
|
1309
|
+
content: `[Debate] Multi-agent debate did not pass (score: ${debateResult.finalScore}). ${debateResult.summary}. Please address the identified issues.`,
|
|
1310
|
+
});
|
|
1311
|
+
this.emitEvent({
|
|
1312
|
+
kind: "agent:thinking",
|
|
1313
|
+
content: `Debate failed (score: ${debateResult.finalScore}). Continuing to address issues...`,
|
|
1314
|
+
});
|
|
1315
|
+
continue; // Continue loop to address debate feedback
|
|
1316
|
+
}
|
|
1317
|
+
this.emitEvent({
|
|
1318
|
+
kind: "agent:thinking",
|
|
1319
|
+
content: `Debate passed (score: ${debateResult.finalScore}). Proceeding to completion.`,
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
catch {
|
|
1323
|
+
// Debate failure is non-fatal — proceed with completion
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
// Persist learnings from self-reflection
|
|
1327
|
+
if (this.memoryManager) {
|
|
1328
|
+
try {
|
|
1329
|
+
await this.selfReflection.persistLearnings(this.memoryManager);
|
|
1330
|
+
}
|
|
1331
|
+
catch {
|
|
1332
|
+
// Learning persistence failure is non-fatal
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
catch {
|
|
1337
|
+
// Deep verification failure is non-fatal — proceed with completion
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
835
1340
|
if (content) {
|
|
836
1341
|
this.contextManager.addMessage({
|
|
837
1342
|
role: "assistant",
|
|
@@ -861,9 +1366,13 @@ export class AgentLoop extends EventEmitter {
|
|
|
861
1366
|
});
|
|
862
1367
|
}
|
|
863
1368
|
// 4. 도구 실행
|
|
864
|
-
const toolResults = await this.executeTools(response.toolCalls);
|
|
1369
|
+
const { results: toolResults, deferredFixPrompts } = await this.executeTools(response.toolCalls);
|
|
865
1370
|
// Reflexion: 도구 결과 수집
|
|
866
1371
|
this.allToolResults.push(...toolResults);
|
|
1372
|
+
// Tool plan tracking: 실행된 도구 이름 기록
|
|
1373
|
+
for (const result of toolResults) {
|
|
1374
|
+
this.executedToolNames.push(result.name);
|
|
1375
|
+
}
|
|
867
1376
|
// 5. 도구 결과를 히스토리에 추가 (살균 + 압축)
|
|
868
1377
|
for (const result of toolResults) {
|
|
869
1378
|
// Prompt injection 방어: 도구 출력 살균
|
|
@@ -883,6 +1392,14 @@ export class AgentLoop extends EventEmitter {
|
|
|
883
1392
|
tool_call_id: result.tool_call_id,
|
|
884
1393
|
});
|
|
885
1394
|
}
|
|
1395
|
+
// AutoFix: deferred fix prompts를 tool results 뒤에 추가
|
|
1396
|
+
// (tool results 전에 넣으면 OpenAI 400 에러 발생 — tool_calls 뒤에 tool 메시지가 바로 와야 함)
|
|
1397
|
+
for (const fixPrompt of deferredFixPrompts) {
|
|
1398
|
+
this.contextManager.addMessage({
|
|
1399
|
+
role: "user",
|
|
1400
|
+
content: fixPrompt,
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
886
1403
|
// iteration 이벤트
|
|
887
1404
|
this.emitEvent({
|
|
888
1405
|
kind: "agent:iteration",
|
|
@@ -890,6 +1407,47 @@ export class AgentLoop extends EventEmitter {
|
|
|
890
1407
|
tokensUsed: response.usage.input + response.usage.output,
|
|
891
1408
|
durationMs: Date.now() - iterationStart,
|
|
892
1409
|
});
|
|
1410
|
+
// Level 1: Quick verification after every 3rd iteration
|
|
1411
|
+
if (this.selfReflection && iteration % 3 === 0) {
|
|
1412
|
+
try {
|
|
1413
|
+
const changedFilesMap = new Map();
|
|
1414
|
+
for (const filePath of this.changedFiles) {
|
|
1415
|
+
// Collect changed file contents from tool results
|
|
1416
|
+
const lastWrite = this.allToolResults
|
|
1417
|
+
.filter((r) => r.name === "file_write" || r.name === "file_edit")
|
|
1418
|
+
.find((r) => r.output.includes(filePath));
|
|
1419
|
+
if (lastWrite) {
|
|
1420
|
+
changedFilesMap.set(filePath, lastWrite.output);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
const quickResult = await this.selfReflection.quickVerify(changedFilesMap, async (prompt) => {
|
|
1424
|
+
const verifyResponse = await this.llmClient.chat([
|
|
1425
|
+
{ role: "system", content: "You are a code verification assistant." },
|
|
1426
|
+
{ role: "user", content: prompt },
|
|
1427
|
+
]);
|
|
1428
|
+
this.tokenBudgetManager.recordUsage("validator", verifyResponse.usage.input, verifyResponse.usage.output);
|
|
1429
|
+
return verifyResponse.content ?? "";
|
|
1430
|
+
});
|
|
1431
|
+
if (quickResult.verdict !== "pass") {
|
|
1432
|
+
const issues = Object.entries(quickResult.dimensions)
|
|
1433
|
+
.filter(([, dim]) => dim.issues.length > 0)
|
|
1434
|
+
.flatMap(([, dim]) => dim.issues);
|
|
1435
|
+
if (issues.length > 0) {
|
|
1436
|
+
this.contextManager.addMessage({
|
|
1437
|
+
role: "system",
|
|
1438
|
+
content: `[Self-Reflection L1] Issues detected: ${issues.join(", ")}. Confidence: ${quickResult.confidence}`,
|
|
1439
|
+
});
|
|
1440
|
+
this.emitEvent({
|
|
1441
|
+
kind: "agent:thinking",
|
|
1442
|
+
content: `Self-reflection flagged ${issues.length} issues (confidence: ${quickResult.confidence.toFixed(2)})`,
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
catch {
|
|
1448
|
+
// Quick verification failure is non-fatal
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
893
1451
|
// 에러가 많으면 FailureRecovery + 리플래닝 시도
|
|
894
1452
|
const errorResults = toolResults.filter((r) => !r.success);
|
|
895
1453
|
if (errorResults.length > 0) {
|
|
@@ -926,8 +1484,9 @@ export class AgentLoop extends EventEmitter {
|
|
|
926
1484
|
content: `Rolled back changes. ${decision.reason}`,
|
|
927
1485
|
});
|
|
928
1486
|
}
|
|
929
|
-
else {
|
|
1487
|
+
else if (this.iterationSystemMsgCount < 5) {
|
|
930
1488
|
// retry, approach_change, scope_reduce → 복구 프롬프트 주입
|
|
1489
|
+
// Context Budget: consolidated error guidance (recovery + debug in one message)
|
|
931
1490
|
const recoveryPrompt = this.failureRecovery.buildRecoveryPrompt(decision, {
|
|
932
1491
|
error: errorSummary,
|
|
933
1492
|
attemptNumber: iteration,
|
|
@@ -936,15 +1495,97 @@ export class AgentLoop extends EventEmitter {
|
|
|
936
1495
|
originalSnapshots: this.originalSnapshots,
|
|
937
1496
|
previousStrategies: this.previousStrategies,
|
|
938
1497
|
});
|
|
1498
|
+
// SelfDebugLoop: merge debug strategy into recovery message (not separate system msg)
|
|
1499
|
+
let debugSuffix = "";
|
|
1500
|
+
if (iteration >= 3) {
|
|
1501
|
+
const rootCauseAnalysis = this.selfDebugLoop.analyzeError(errorSummary);
|
|
1502
|
+
if (rootCauseAnalysis.confidence >= 0.5) {
|
|
1503
|
+
const debugStrategy = this.selfDebugLoop.selectStrategy(iteration - 2, []);
|
|
1504
|
+
const debugPrompt = this.selfDebugLoop.buildFixPrompt(debugStrategy, {
|
|
1505
|
+
testCommand: "pnpm build",
|
|
1506
|
+
errorOutput: errorSummary,
|
|
1507
|
+
changedFiles: this.changedFiles,
|
|
1508
|
+
originalSnapshots: this.originalSnapshots,
|
|
1509
|
+
previousAttempts: [],
|
|
1510
|
+
currentStrategy: debugStrategy,
|
|
1511
|
+
});
|
|
1512
|
+
debugSuffix = `\n\n[SelfDebug L${Math.min(iteration - 2, 5)}] Strategy: ${debugStrategy}\n${debugPrompt}`;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
939
1515
|
this.contextManager.addMessage({
|
|
940
1516
|
role: "system",
|
|
941
|
-
content: recoveryPrompt,
|
|
1517
|
+
content: recoveryPrompt + debugSuffix,
|
|
942
1518
|
});
|
|
1519
|
+
this.iterationSystemMsgCount++;
|
|
943
1520
|
}
|
|
944
1521
|
// HierarchicalPlanner 리플래닝도 시도
|
|
945
1522
|
if (this.activePlan) {
|
|
946
1523
|
await this.attemptReplan(errorSummary);
|
|
947
1524
|
}
|
|
1525
|
+
// 에러 트리거 예산 비율 계산 (SkillLearner + Plugin 모두 사용)
|
|
1526
|
+
const errorTriggerBudgetRatio = this.config.loop.totalTokenBudget > 0
|
|
1527
|
+
? this.tokenUsage.total / this.config.loop.totalTokenBudget
|
|
1528
|
+
: 0;
|
|
1529
|
+
// Context Budget: skip all optional injections at 85%+ budget
|
|
1530
|
+
if (errorTriggerBudgetRatio <= 0.85 && this.iterationSystemMsgCount < 5) {
|
|
1531
|
+
// SkillLearner: 학습된 스킬 중 현재 에러에 매칭되는 것 주입
|
|
1532
|
+
if (this.skillLearner && this.activeSkillIds.length < AgentLoop.MAX_ACTIVE_SKILLS) {
|
|
1533
|
+
try {
|
|
1534
|
+
const relevantSkills = this.skillLearner.getRelevantSkills({
|
|
1535
|
+
errorMessage: errorSummary,
|
|
1536
|
+
language: undefined,
|
|
1537
|
+
});
|
|
1538
|
+
for (const skill of relevantSkills.slice(0, 1)) {
|
|
1539
|
+
if (this.activeSkillIds.includes(skill.id))
|
|
1540
|
+
continue;
|
|
1541
|
+
this.contextManager.addMessage({
|
|
1542
|
+
role: "system",
|
|
1543
|
+
content: `[Learned Skill: ${skill.id}] Diagnosis: ${skill.diagnosis}\nStrategy: ${skill.strategy}\nTools: ${skill.toolSequence.join(" → ")}`,
|
|
1544
|
+
});
|
|
1545
|
+
this.activeSkillIds.push(skill.id);
|
|
1546
|
+
this.iterationSystemMsgCount++;
|
|
1547
|
+
this.skillLearner.updateConfidence(skill.id, false);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
catch {
|
|
1551
|
+
// 학습 스킬 매칭 실패는 치명적이지 않음
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
// Plugin trigger matching — match errors/context to plugin skills
|
|
1555
|
+
if (this.activeSkillIds.length < AgentLoop.MAX_ACTIVE_SKILLS &&
|
|
1556
|
+
this.iterationSystemMsgCount < 5) {
|
|
1557
|
+
const triggerMatches = this.pluginRegistry.matchTriggers({
|
|
1558
|
+
errorMessage: errorSummary,
|
|
1559
|
+
taskDescription: "",
|
|
1560
|
+
});
|
|
1561
|
+
if (triggerMatches.length > 0) {
|
|
1562
|
+
const bestMatch = triggerMatches[0];
|
|
1563
|
+
if (bestMatch.skill && !this.activeSkillIds.includes(bestMatch.skill.id)) {
|
|
1564
|
+
const skillTemplate = this.skillLoader.loadTemplate(bestMatch.skill);
|
|
1565
|
+
if (skillTemplate) {
|
|
1566
|
+
let resolved = this.skillLoader.resolveTemplate(skillTemplate.content, {
|
|
1567
|
+
error: errorSummary,
|
|
1568
|
+
files: this.changedFiles,
|
|
1569
|
+
});
|
|
1570
|
+
const MAX_SKILL_INJECT_CHARS = 2000;
|
|
1571
|
+
if (resolved.length > MAX_SKILL_INJECT_CHARS) {
|
|
1572
|
+
resolved = resolved.slice(0, MAX_SKILL_INJECT_CHARS) + "\n[...truncated for token budget]";
|
|
1573
|
+
}
|
|
1574
|
+
this.contextManager.addMessage({
|
|
1575
|
+
role: "system",
|
|
1576
|
+
content: `[Plugin Skill: ${bestMatch.pluginId}/${bestMatch.skill.name}]\n${resolved}`,
|
|
1577
|
+
});
|
|
1578
|
+
this.activeSkillIds.push(bestMatch.skill.id);
|
|
1579
|
+
this.iterationSystemMsgCount++;
|
|
1580
|
+
this.emitEvent({
|
|
1581
|
+
kind: "agent:thinking",
|
|
1582
|
+
content: `Activated plugin skill: ${bestMatch.skill.name} from ${bestMatch.pluginId}`,
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
948
1589
|
}
|
|
949
1590
|
// 체크포인트 저장: 토큰 예산 80% 이상 사용 시 자동 저장 (1회만)
|
|
950
1591
|
if (!this.checkpointSaved &&
|
|
@@ -952,6 +1593,18 @@ export class AgentLoop extends EventEmitter {
|
|
|
952
1593
|
await this.saveAutoCheckpoint(iteration);
|
|
953
1594
|
this.checkpointSaved = true;
|
|
954
1595
|
}
|
|
1596
|
+
// ContinuousReflection: 매 5 iteration마다 비상 체크포인트 트리거
|
|
1597
|
+
// (정기 타이머 외에 iteration 기반 추가 안전망)
|
|
1598
|
+
if (this.continuousReflection?.isRunning() &&
|
|
1599
|
+
iteration > 0 &&
|
|
1600
|
+
iteration % 5 === 0) {
|
|
1601
|
+
try {
|
|
1602
|
+
await this.continuousReflection.emergencyCheckpoint();
|
|
1603
|
+
}
|
|
1604
|
+
catch {
|
|
1605
|
+
// reflection 체크포인트 실패는 치명적이지 않음
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
955
1608
|
// 예산 초과 체크
|
|
956
1609
|
if (this.tokenUsage.total >= this.config.loop.totalTokenBudget) {
|
|
957
1610
|
return {
|
|
@@ -1050,6 +1703,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
1050
1703
|
*/
|
|
1051
1704
|
async executeTools(toolCalls) {
|
|
1052
1705
|
const results = [];
|
|
1706
|
+
const deferredFixPrompts = [];
|
|
1053
1707
|
for (const toolCall of toolCalls) {
|
|
1054
1708
|
// Governor: 안전성 검증
|
|
1055
1709
|
try {
|
|
@@ -1081,6 +1735,29 @@ export class AgentLoop extends EventEmitter {
|
|
|
1081
1735
|
}
|
|
1082
1736
|
// 승인됨 → 계속 실행
|
|
1083
1737
|
}
|
|
1738
|
+
// Plugin Tool Approval Gate: check plugin tools requiring approval
|
|
1739
|
+
const pluginTools = this.pluginRegistry.getAllTools();
|
|
1740
|
+
const matchedPluginTool = pluginTools.find((pt) => pt.tool.name === toolCall.name);
|
|
1741
|
+
if (matchedPluginTool &&
|
|
1742
|
+
(matchedPluginTool.tool.requiresApproval === true ||
|
|
1743
|
+
matchedPluginTool.tool.sideEffectLevel === "destructive")) {
|
|
1744
|
+
const pluginApprovalReq = {
|
|
1745
|
+
id: `plugin-approval-${toolCall.id}`,
|
|
1746
|
+
toolName: toolCall.name,
|
|
1747
|
+
arguments: args,
|
|
1748
|
+
reason: `Plugin tool "${toolCall.name}" (from ${matchedPluginTool.pluginId}) requires approval (${matchedPluginTool.tool.sideEffectLevel === "destructive"
|
|
1749
|
+
? "destructive side effect"
|
|
1750
|
+
: "requiresApproval=true"})`,
|
|
1751
|
+
riskLevel: matchedPluginTool.tool.riskLevel === "high" ? "high" : "medium",
|
|
1752
|
+
timeout: 120_000,
|
|
1753
|
+
};
|
|
1754
|
+
const pluginApprovalResult = await this.handleApprovalRequest(toolCall, pluginApprovalReq);
|
|
1755
|
+
if (pluginApprovalResult) {
|
|
1756
|
+
results.push(pluginApprovalResult);
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
// Approved → proceed with execution
|
|
1760
|
+
}
|
|
1084
1761
|
// MCP 도구 호출 확인
|
|
1085
1762
|
if (this.mcpClient && this.isMCPTool(toolCall.name)) {
|
|
1086
1763
|
const mcpResult = await this.executeMCPTool(toolCall);
|
|
@@ -1143,8 +1820,11 @@ export class AgentLoop extends EventEmitter {
|
|
|
1143
1820
|
this.analyzeFileImpact(filePathStr).catch(() => { });
|
|
1144
1821
|
}
|
|
1145
1822
|
}
|
|
1146
|
-
// AutoFixLoop: 결과 검증
|
|
1147
|
-
await this.validateAndFeedback(toolCall.name, result);
|
|
1823
|
+
// AutoFixLoop: 결과 검증 (fix prompt는 tool results 추가 후 context에 넣음)
|
|
1824
|
+
const fixPrompt = await this.validateAndFeedback(toolCall.name, result);
|
|
1825
|
+
if (fixPrompt) {
|
|
1826
|
+
deferredFixPrompts.push(fixPrompt);
|
|
1827
|
+
}
|
|
1148
1828
|
}
|
|
1149
1829
|
catch (err) {
|
|
1150
1830
|
this.interruptManager.clearToolAbort();
|
|
@@ -1163,8 +1843,18 @@ export class AgentLoop extends EventEmitter {
|
|
|
1163
1843
|
message: `Tool ${toolCall.name} cancelled by interrupt`,
|
|
1164
1844
|
retryable: false,
|
|
1165
1845
|
});
|
|
1166
|
-
//
|
|
1167
|
-
|
|
1846
|
+
// 남은 tool calls에 대해 placeholder result 추가 (OpenAI 400 방지)
|
|
1847
|
+
const currentIdx = toolCalls.indexOf(toolCall);
|
|
1848
|
+
for (let i = currentIdx + 1; i < toolCalls.length; i++) {
|
|
1849
|
+
results.push({
|
|
1850
|
+
tool_call_id: toolCalls[i].id,
|
|
1851
|
+
name: toolCalls[i].name,
|
|
1852
|
+
output: `[SKIPPED] Previous tool was interrupted.`,
|
|
1853
|
+
success: false,
|
|
1854
|
+
durationMs: 0,
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
// soft interrupt: 루프 계속 / hard interrupt: aborted=true로 종료
|
|
1168
1858
|
break;
|
|
1169
1859
|
}
|
|
1170
1860
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -1182,7 +1872,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
1182
1872
|
});
|
|
1183
1873
|
}
|
|
1184
1874
|
}
|
|
1185
|
-
return results;
|
|
1875
|
+
return { results, deferredFixPrompts };
|
|
1186
1876
|
}
|
|
1187
1877
|
/**
|
|
1188
1878
|
* Governor의 ApprovalRequiredError를 ApprovalManager로 처리.
|
|
@@ -1229,13 +1919,13 @@ export class AgentLoop extends EventEmitter {
|
|
|
1229
1919
|
async validateAndFeedback(toolName, result) {
|
|
1230
1920
|
// file_write/file_edit만 검증 (다른 도구는 스킵)
|
|
1231
1921
|
if (!["file_write", "file_edit"].includes(toolName)) {
|
|
1232
|
-
return;
|
|
1922
|
+
return null;
|
|
1233
1923
|
}
|
|
1234
1924
|
const validation = await this.autoFixLoop.validateResult(toolName, result.output, result.success, this.config.loop.projectPath);
|
|
1235
1925
|
if (validation.passed) {
|
|
1236
1926
|
// 검증 통과 → 수정 시도 기록 초기화
|
|
1237
1927
|
this.autoFixLoop.resetAttempts();
|
|
1238
|
-
return;
|
|
1928
|
+
return null;
|
|
1239
1929
|
}
|
|
1240
1930
|
// 검증 실패 → 에러 피드백
|
|
1241
1931
|
if (!this.autoFixLoop.canRetry()) {
|
|
@@ -1245,20 +1935,17 @@ export class AgentLoop extends EventEmitter {
|
|
|
1245
1935
|
message: `Auto-fix exhausted (${this.autoFixLoop.getAttempts().length} attempts): ${validation.failures[0]?.message ?? "Unknown error"}`,
|
|
1246
1936
|
retryable: false,
|
|
1247
1937
|
});
|
|
1248
|
-
return;
|
|
1938
|
+
return null;
|
|
1249
1939
|
}
|
|
1250
|
-
// 수정 프롬프트 생성
|
|
1940
|
+
// 수정 프롬프트 생성 — 히스토리 추가는 caller가 tool result 추가 후 수행
|
|
1251
1941
|
const errorMsg = validation.failures
|
|
1252
1942
|
.map((f) => `[${f.type}] ${f.message}\n${f.rawOutput}`)
|
|
1253
1943
|
.join("\n\n");
|
|
1254
1944
|
const fixPrompt = this.autoFixLoop.buildFixPrompt(errorMsg, `After ${toolName} execution on project at ${this.config.loop.projectPath}`);
|
|
1255
1945
|
// 수정 시도 기록
|
|
1256
1946
|
this.autoFixLoop.recordAttempt(errorMsg, "Requesting LLM fix", false, 0);
|
|
1257
|
-
//
|
|
1258
|
-
|
|
1259
|
-
role: "user",
|
|
1260
|
-
content: fixPrompt,
|
|
1261
|
-
});
|
|
1947
|
+
// fixPrompt를 반환 — caller가 tool result 메시지 추가 후 context에 넣음
|
|
1948
|
+
return fixPrompt;
|
|
1262
1949
|
}
|
|
1263
1950
|
/**
|
|
1264
1951
|
* 도구 인자를 파싱하는 헬퍼.
|