@yuaone/core 0.9.43 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-affordance.d.ts +37 -0
- package/dist/agent-affordance.d.ts.map +1 -0
- package/dist/agent-affordance.js +139 -0
- package/dist/agent-affordance.js.map +1 -0
- package/dist/agent-decision-types.d.ts +262 -0
- package/dist/agent-decision-types.d.ts.map +1 -0
- package/dist/agent-decision-types.js +19 -0
- package/dist/agent-decision-types.js.map +1 -0
- package/dist/agent-decision.d.ts +52 -0
- package/dist/agent-decision.d.ts.map +1 -0
- package/dist/agent-decision.js +767 -0
- package/dist/agent-decision.js.map +1 -0
- package/dist/agent-loop.d.ts +37 -79
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +730 -586
- package/dist/agent-loop.js.map +1 -1
- package/dist/agent-reasoning-engine.d.ts +48 -0
- package/dist/agent-reasoning-engine.d.ts.map +1 -0
- package/dist/agent-reasoning-engine.js +544 -0
- package/dist/agent-reasoning-engine.js.map +1 -0
- package/dist/codebase-context.d.ts +3 -0
- package/dist/codebase-context.d.ts.map +1 -1
- package/dist/codebase-context.js +15 -6
- package/dist/codebase-context.js.map +1 -1
- package/dist/command-plan-compiler.d.ts +43 -0
- package/dist/command-plan-compiler.d.ts.map +1 -0
- package/dist/command-plan-compiler.js +164 -0
- package/dist/command-plan-compiler.js.map +1 -0
- package/dist/dependency-guard.d.ts +18 -0
- package/dist/dependency-guard.d.ts.map +1 -0
- package/dist/dependency-guard.js +113 -0
- package/dist/dependency-guard.js.map +1 -0
- package/dist/execution-engine.d.ts +10 -1
- package/dist/execution-engine.d.ts.map +1 -1
- package/dist/execution-engine.js +162 -8
- package/dist/execution-engine.js.map +1 -1
- package/dist/execution-receipt.d.ts +62 -0
- package/dist/execution-receipt.d.ts.map +1 -0
- package/dist/execution-receipt.js +67 -0
- package/dist/execution-receipt.js.map +1 -0
- package/dist/failure-surface-writer.d.ts +13 -0
- package/dist/failure-surface-writer.d.ts.map +1 -0
- package/dist/failure-surface-writer.js +33 -0
- package/dist/failure-surface-writer.js.map +1 -0
- package/dist/file-chunker.d.ts +26 -0
- package/dist/file-chunker.d.ts.map +1 -0
- package/dist/file-chunker.js +103 -0
- package/dist/file-chunker.js.map +1 -0
- package/dist/image-observer.d.ts +22 -0
- package/dist/image-observer.d.ts.map +1 -0
- package/dist/image-observer.js +60 -0
- package/dist/image-observer.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -1
- package/dist/judgment-rules.d.ts +44 -0
- package/dist/judgment-rules.d.ts.map +1 -0
- package/dist/judgment-rules.js +185 -0
- package/dist/judgment-rules.js.map +1 -0
- package/dist/memory-decay.d.ts +41 -0
- package/dist/memory-decay.d.ts.map +1 -0
- package/dist/memory-decay.js +62 -0
- package/dist/memory-decay.js.map +1 -0
- package/dist/memory-manager.d.ts.map +1 -1
- package/dist/memory-manager.js +30 -0
- package/dist/memory-manager.js.map +1 -1
- package/dist/model-weakness-tracker.d.ts +42 -0
- package/dist/model-weakness-tracker.d.ts.map +1 -0
- package/dist/model-weakness-tracker.js +107 -0
- package/dist/model-weakness-tracker.js.map +1 -0
- package/dist/overhead-governor.d.ts +3 -1
- package/dist/overhead-governor.d.ts.map +1 -1
- package/dist/overhead-governor.js +5 -0
- package/dist/overhead-governor.js.map +1 -1
- package/dist/patch-scope-controller.d.ts +44 -0
- package/dist/patch-scope-controller.d.ts.map +1 -0
- package/dist/patch-scope-controller.js +107 -0
- package/dist/patch-scope-controller.js.map +1 -0
- package/dist/patch-transaction.d.ts +53 -0
- package/dist/patch-transaction.d.ts.map +1 -0
- package/dist/patch-transaction.js +119 -0
- package/dist/patch-transaction.js.map +1 -0
- package/dist/pre-write-validator.d.ts +29 -0
- package/dist/pre-write-validator.d.ts.map +1 -0
- package/dist/pre-write-validator.js +97 -0
- package/dist/pre-write-validator.js.map +1 -0
- package/dist/prompt-builder.d.ts +25 -0
- package/dist/prompt-builder.d.ts.map +1 -0
- package/dist/prompt-builder.js +93 -0
- package/dist/prompt-builder.js.map +1 -0
- package/dist/prompt-envelope.d.ts +40 -0
- package/dist/prompt-envelope.d.ts.map +1 -0
- package/dist/prompt-envelope.js +16 -0
- package/dist/prompt-envelope.js.map +1 -0
- package/dist/prompt-runtime.d.ts +66 -0
- package/dist/prompt-runtime.d.ts.map +1 -0
- package/dist/prompt-runtime.js +492 -0
- package/dist/prompt-runtime.js.map +1 -0
- package/dist/repo-capability-profile.d.ts +24 -0
- package/dist/repo-capability-profile.d.ts.map +1 -0
- package/dist/repo-capability-profile.js +113 -0
- package/dist/repo-capability-profile.js.map +1 -0
- package/dist/security-gate.d.ts +39 -0
- package/dist/security-gate.d.ts.map +1 -0
- package/dist/security-gate.js +121 -0
- package/dist/security-gate.js.map +1 -0
- package/dist/self-evaluation.d.ts +22 -0
- package/dist/self-evaluation.d.ts.map +1 -0
- package/dist/self-evaluation.js +43 -0
- package/dist/self-evaluation.js.map +1 -0
- package/dist/semantic-diff-reviewer.d.ts +28 -0
- package/dist/semantic-diff-reviewer.d.ts.map +1 -0
- package/dist/semantic-diff-reviewer.js +168 -0
- package/dist/semantic-diff-reviewer.js.map +1 -0
- package/dist/stall-detector.d.ts +26 -2
- package/dist/stall-detector.d.ts.map +1 -1
- package/dist/stall-detector.js +128 -3
- package/dist/stall-detector.js.map +1 -1
- package/dist/system-core.d.ts +27 -0
- package/dist/system-core.d.ts.map +1 -0
- package/dist/system-core.js +269 -0
- package/dist/system-core.js.map +1 -0
- package/dist/system-prompt.d.ts +4 -0
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +12 -218
- package/dist/system-prompt.js.map +1 -1
- package/dist/target-file-ranker.d.ts +38 -0
- package/dist/target-file-ranker.d.ts.map +1 -0
- package/dist/target-file-ranker.js +90 -0
- package/dist/target-file-ranker.js.map +1 -0
- package/dist/task-classifier.d.ts +6 -0
- package/dist/task-classifier.d.ts.map +1 -1
- package/dist/task-classifier.js +6 -0
- package/dist/task-classifier.js.map +1 -1
- package/dist/test-impact-planner.d.ts +16 -0
- package/dist/test-impact-planner.d.ts.map +1 -0
- package/dist/test-impact-planner.js +68 -0
- package/dist/test-impact-planner.js.map +1 -0
- package/dist/tool-outcome-cache.d.ts +41 -0
- package/dist/tool-outcome-cache.d.ts.map +1 -0
- package/dist/tool-outcome-cache.js +88 -0
- package/dist/tool-outcome-cache.js.map +1 -0
- package/dist/types.d.ts +39 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/verifier-rules.d.ts +15 -0
- package/dist/verifier-rules.d.ts.map +1 -0
- package/dist/verifier-rules.js +80 -0
- package/dist/verifier-rules.js.map +1 -0
- package/dist/workspace-mutation-policy.d.ts +28 -0
- package/dist/workspace-mutation-policy.d.ts.map +1 -0
- package/dist/workspace-mutation-policy.js +56 -0
- package/dist/workspace-mutation-policy.js.map +1 -0
- package/package.json +1 -1
package/dist/agent-loop.js
CHANGED
|
@@ -21,6 +21,8 @@ import { InterruptManager } from "./interrupt-manager.js";
|
|
|
21
21
|
import { YuanMemory } from "./memory.js";
|
|
22
22
|
import { MemoryManager } from "./memory-manager.js";
|
|
23
23
|
import { buildSystemPrompt } from "./system-prompt.js";
|
|
24
|
+
import { compilePromptEnvelope } from "./prompt-runtime.js";
|
|
25
|
+
import { buildPrompt } from "./prompt-builder.js";
|
|
24
26
|
import { HierarchicalPlanner, } from "./hierarchical-planner.js";
|
|
25
27
|
import { TaskClassifier } from "./task-classifier.js";
|
|
26
28
|
import { PromptDefense } from "./prompt-defense.js";
|
|
@@ -55,12 +57,14 @@ import { ContextBudgetManager } from "./context-budget.js";
|
|
|
55
57
|
import { QAPipeline } from "./qa-pipeline.js";
|
|
56
58
|
import { PersonaManager } from "./persona.js";
|
|
57
59
|
import { InMemoryVectorStore, OllamaEmbeddingProvider } from "./vector-store.js";
|
|
60
|
+
import { agentDecide, DEFAULT_DECISION, worldStateToProjectContext } from "./agent-decision.js";
|
|
58
61
|
import { OverheadGovernor } from "./overhead-governor.js";
|
|
59
62
|
import { StateStore, TransitionModel, SimulationEngine, StateUpdater, } from "./world-model/index.js";
|
|
60
63
|
import { MilestoneChecker, RiskEstimator, PlanEvaluator, ReplanningEngine, } from "./planner/index.js";
|
|
61
64
|
import { TraceRecorder } from "./trace-recorder.js";
|
|
62
65
|
import { ArchSummarizer } from "./arch-summarizer.js";
|
|
63
66
|
import { FailureSignatureMemory } from "./failure-signature-memory.js";
|
|
67
|
+
import { CausalChainResolver } from "./causal-chain-resolver.js";
|
|
64
68
|
import { PlaybookLibrary } from "./playbook-library.js";
|
|
65
69
|
import { ProjectExecutive } from "./project-executive.js";
|
|
66
70
|
import { StallDetector } from "./stall-detector.js";
|
|
@@ -81,37 +85,20 @@ import { registerToolsInGraph, recordToolOutcomeInGraph } from "./extensions/cap
|
|
|
81
85
|
import { getSelfWeaknessContext } from "./extensions/self-model-wiring.js";
|
|
82
86
|
import { initMarketPlaybooks, selectMarketStrategy } from "./extensions/strategy-wiring.js";
|
|
83
87
|
import { VisionIntentDetector } from "./vision-intent-detector.js";
|
|
88
|
+
import { PatchTransactionJournal } from "./patch-transaction.js";
|
|
89
|
+
import { securityCheck } from "./security-gate.js";
|
|
90
|
+
import { JudgmentRuleRegistry } from "./judgment-rules.js";
|
|
91
|
+
import { loadOrScanProfile } from "./repo-capability-profile.js";
|
|
92
|
+
import { verifyToolResult } from "./verifier-rules.js";
|
|
93
|
+
import { checkDependencyChange } from "./dependency-guard.js";
|
|
94
|
+
import { WorkspaceMutationPolicy } from "./workspace-mutation-policy.js";
|
|
95
|
+
import { validateBeforeWrite, detectFileRole } from "./pre-write-validator.js";
|
|
96
|
+
import { PatchScopeController, detectRepoLifecycle } from "./patch-scope-controller.js";
|
|
97
|
+
import { classifyCommand, validateProposedCommand, compileVerifyCommands } from "./command-plan-compiler.js";
|
|
98
|
+
import { reviewFileDiff } from "./semantic-diff-reviewer.js";
|
|
99
|
+
import { ModelWeaknessTracker } from "./model-weakness-tracker.js";
|
|
84
100
|
import { dlog, dlogSep } from "./debug-logger.js";
|
|
85
|
-
/**
|
|
86
|
-
* AgentLoop — YUAN 에이전트의 핵심 실행 루프.
|
|
87
|
-
*
|
|
88
|
-
* 동작 흐름:
|
|
89
|
-
* 1. 사용자 메시지 수신
|
|
90
|
-
* 2. 시스템 프롬프트 + 히스토리로 LLM 호출
|
|
91
|
-
* 3. LLM 응답에서 tool_call 파싱
|
|
92
|
-
* 4. Governor가 안전성 검증
|
|
93
|
-
* 5. 도구 실행 → 결과를 히스토리에 추가
|
|
94
|
-
* 6. LLM에 결과 피드백 → 2번으로 반복
|
|
95
|
-
* 7. 종료 조건 충족 시 결과 반환
|
|
96
|
-
*
|
|
97
|
-
* @example
|
|
98
|
-
* ```typescript
|
|
99
|
-
* const loop = new AgentLoop({
|
|
100
|
-
* config: agentConfig,
|
|
101
|
-
* toolExecutor: executor,
|
|
102
|
-
* governorConfig: { planTier: "PRO" },
|
|
103
|
-
* });
|
|
104
|
-
*
|
|
105
|
-
* loop.on("event", (event: AgentEvent) => {
|
|
106
|
-
* // SSE 스트리밍
|
|
107
|
-
* });
|
|
108
|
-
*
|
|
109
|
-
* const result = await loop.run("모든 console.log를 제거해줘");
|
|
110
|
-
* ```
|
|
111
|
-
*/
|
|
112
|
-
/** Minimum confidence for classification-based hints/routing to activate */
|
|
113
101
|
const CLASSIFICATION_CONFIDENCE_THRESHOLD = 0.6;
|
|
114
|
-
/** spawn_sub_agent tool definition — allows the LLM to delegate tasks to sub-agents */
|
|
115
102
|
const SPAWN_SUB_AGENT_TOOL = {
|
|
116
103
|
name: "spawn_sub_agent",
|
|
117
104
|
description: "Spawn a sub-agent to work on a delegated task in parallel. " +
|
|
@@ -167,14 +154,19 @@ export class AgentLoop extends EventEmitter {
|
|
|
167
154
|
approvalManager;
|
|
168
155
|
autoFixLoop;
|
|
169
156
|
interruptManager;
|
|
157
|
+
/** @deprecated Use decision.core.memoryLoad.shouldLoad at runtime. Kept for constructor init only. */
|
|
170
158
|
enableMemory;
|
|
159
|
+
/** @deprecated Use decision.core.planRequired at runtime. Kept for constructor init only. */
|
|
171
160
|
enablePlanning;
|
|
161
|
+
/** @deprecated Planning threshold is now determined by Decision Engine. Kept for constructor init only. */
|
|
172
162
|
planningThreshold;
|
|
173
163
|
environment;
|
|
174
164
|
yuanMemory = null;
|
|
175
165
|
memoryManager = null;
|
|
166
|
+
judgmentRegistry = null;
|
|
176
167
|
planner = null;
|
|
177
168
|
activePlan = null;
|
|
169
|
+
/** @deprecated Use AgentDecisionContext when available; taskClassifier is legacy fallback only */
|
|
178
170
|
taskClassifier;
|
|
179
171
|
promptDefense;
|
|
180
172
|
reflexionEngine = null;
|
|
@@ -205,6 +197,23 @@ export class AgentLoop extends EventEmitter {
|
|
|
205
197
|
enableSelfReflection;
|
|
206
198
|
enableDebate;
|
|
207
199
|
currentComplexity = "simple";
|
|
200
|
+
// ─── Agent Decision Engine ─────────────────────────────────────────────────
|
|
201
|
+
decision = DEFAULT_DECISION;
|
|
202
|
+
// ─── Prompt 3-Layer Architecture ──────────────────────────────────────────
|
|
203
|
+
pendingRunContext = {};
|
|
204
|
+
/** Cached values for refreshSystemPrompt() — set during criticalInit/backgroundInit */
|
|
205
|
+
_cachedProjectStructure;
|
|
206
|
+
_cachedYuanMdContent;
|
|
207
|
+
prevDecision = DEFAULT_DECISION;
|
|
208
|
+
toolUsageCounter = {
|
|
209
|
+
reads: 0,
|
|
210
|
+
edits: 0,
|
|
211
|
+
shells: 0,
|
|
212
|
+
tests: 0,
|
|
213
|
+
searches: 0,
|
|
214
|
+
webLookups: 0,
|
|
215
|
+
sameFileEdits: new Map(),
|
|
216
|
+
};
|
|
208
217
|
policyOverrides;
|
|
209
218
|
checkpointSaved = false;
|
|
210
219
|
iterationCount = 0;
|
|
@@ -224,33 +233,35 @@ export class AgentLoop extends EventEmitter {
|
|
|
224
233
|
backgroundAgentManager = null;
|
|
225
234
|
sessionPersistence = null;
|
|
226
235
|
sessionId = null;
|
|
236
|
+
/** @deprecated Use decision.core.skillActivation.enableToolPlanning at runtime. Kept for constructor init only. */
|
|
227
237
|
enableToolPlanning;
|
|
238
|
+
/** @deprecated Use decision.core.skillActivation.enableSkillLearning at runtime. Kept for constructor init only. */
|
|
228
239
|
enableSkillLearning;
|
|
240
|
+
/** @deprecated Use decision.core.subAgentPlan.enabled at runtime. Kept for constructor init only. */
|
|
229
241
|
enableBackgroundAgents;
|
|
230
242
|
currentToolPlan = null;
|
|
231
243
|
executedToolNames = [];
|
|
232
244
|
/** Unfulfilled-intent continuation counter — reset each run(), max 3 nudges */
|
|
233
245
|
_unfulfilledContinuations = 0;
|
|
246
|
+
/** Model weakness learning layer — scoped by model+repo */
|
|
247
|
+
weaknessTracker = null;
|
|
234
248
|
/** Context Budget: max 3 active skills at once */
|
|
235
249
|
activeSkillIds = [];
|
|
236
250
|
static MAX_ACTIVE_SKILLS = 3;
|
|
237
251
|
/** Context Budget: track injected system messages per iteration to cap at 5 */
|
|
238
252
|
iterationSystemMsgCount = 0;
|
|
239
|
-
/** Task 1: ContextBudgetManager for LLM-based summarization at 60-70% context usage */
|
|
240
253
|
contextBudgetManager = null;
|
|
241
|
-
/** Task 1: Flag to ensure LLM summarization only runs once per agent run (non-blocking guard) */
|
|
242
254
|
_contextSummarizationDone = false;
|
|
243
|
-
/** Task 2: Track whether write tools ran this iteration for QA triggering */
|
|
244
255
|
iterationWriteToolPaths = [];
|
|
245
|
-
/** Task 2: Last QA result (surfaced to LLM on issues) */
|
|
246
256
|
lastQAResult = null;
|
|
247
|
-
/** Task 3: Track TS files modified this run for auto-tsc */
|
|
248
257
|
iterationTsFilesModified = [];
|
|
249
|
-
/** Task 3: Whether tsc was run in the previous iteration (skip cooldown) */
|
|
250
258
|
tscRanLastIteration = false;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
259
|
+
patchJournal = null;
|
|
260
|
+
mutationPolicy = null;
|
|
261
|
+
patchScopeController = null;
|
|
262
|
+
/** Per-iteration ephemeral hints — collected and bulk-injected before next LLM call */
|
|
263
|
+
ephemeralHints = [];
|
|
264
|
+
// ─── OverheadGovernor ──────────────────────────────────────────────────
|
|
254
265
|
overheadGovernor;
|
|
255
266
|
/** Writes since last verify ran (for QA/quickVerify trigger) */
|
|
256
267
|
writeCountSinceVerify = 0;
|
|
@@ -303,14 +314,11 @@ export class AgentLoop extends EventEmitter {
|
|
|
303
314
|
selfImprovementLoop = null;
|
|
304
315
|
metaLearningCollector = null;
|
|
305
316
|
trustEconomics = null;
|
|
306
|
-
// Phase 5: Strategy Learner + Skill Registry
|
|
307
317
|
strategyLearner = null;
|
|
308
318
|
skillRegistry = null;
|
|
309
|
-
// Phase 5 extended: TracePatternExtractor, MetaLearningEngine, ToolSynthesizer
|
|
310
319
|
tracePatternExtractor = null;
|
|
311
320
|
metaLearningEngine = null;
|
|
312
321
|
toolSynthesizer = null;
|
|
313
|
-
// Phase 6: BudgetGovernorV2, CapabilityGraph, CapabilitySelfModel, StrategyMarket
|
|
314
322
|
budgetGovernorV2 = null;
|
|
315
323
|
capabilityGraph = null;
|
|
316
324
|
capabilitySelfModel = null;
|
|
@@ -385,30 +393,23 @@ export class AgentLoop extends EventEmitter {
|
|
|
385
393
|
this.environment = options.environment;
|
|
386
394
|
this.enableSelfReflection = options.enableSelfReflection !== false;
|
|
387
395
|
this.enableDebate = options.enableDebate !== false;
|
|
388
|
-
// BYOK LLM 클라이언트 생성
|
|
389
396
|
this.llmClient = new BYOKClient(options.config.byok);
|
|
390
|
-
// Governor 생성
|
|
391
397
|
this.governor = new Governor(options.governorConfig);
|
|
392
|
-
// OverheadGovernor — subsystem execution policy
|
|
393
398
|
this.overheadGovernor = new OverheadGovernor(options.overheadGovernorConfig, (subsystem, reason) => {
|
|
394
399
|
// Shadow log — emit as thinking event so TUI can see it (dimmed)
|
|
395
400
|
this.emitEvent({ kind: "agent:thinking", content: `[shadow] ${subsystem}: ${reason}` });
|
|
396
401
|
});
|
|
397
|
-
// ContextManager 생성
|
|
398
402
|
this.contextManager = new ContextManager({
|
|
399
403
|
maxContextTokens: options.contextConfig?.maxContextTokens ??
|
|
400
404
|
options.config.loop.totalTokenBudget,
|
|
401
405
|
outputReserveTokens: options.contextConfig?.outputReserveTokens ?? 4096,
|
|
402
406
|
...options.contextConfig,
|
|
403
407
|
});
|
|
404
|
-
// ApprovalManager 생성
|
|
405
408
|
this.approvalManager = new ApprovalManager(options.approvalConfig);
|
|
406
409
|
if (options.approvalHandler) {
|
|
407
410
|
this.approvalManager.setHandler(options.approvalHandler);
|
|
408
411
|
}
|
|
409
|
-
// AutoFixLoop 생성
|
|
410
412
|
this.autoFixLoop = new AutoFixLoop(options.autoFixConfig);
|
|
411
|
-
// Task Classifier + Prompt Defense + Token Budget + Memory Updater
|
|
412
413
|
this.taskClassifier = new TaskClassifier();
|
|
413
414
|
this.promptDefense = new PromptDefense();
|
|
414
415
|
this.tokenBudgetManager = new TokenBudgetManager({
|
|
@@ -418,22 +419,17 @@ export class AgentLoop extends EventEmitter {
|
|
|
418
419
|
this.failureRecovery = new FailureRecovery();
|
|
419
420
|
this.costOptimizer = new CostOptimizer();
|
|
420
421
|
this.policyOverrides = options.policyOverrides;
|
|
421
|
-
// MCP 서버 설정 저장
|
|
422
422
|
this.mcpServerConfigs = options.mcpServerConfigs ?? [];
|
|
423
|
-
// InterruptManager 설정 (외부 주입 또는 내부 생성)
|
|
424
423
|
this.interruptManager = options.interruptManager ?? new InterruptManager();
|
|
425
424
|
this.setupInterruptListeners();
|
|
426
|
-
// PluginRegistry + SkillLoader 초기화
|
|
427
425
|
this.pluginRegistry = options.pluginRegistry ?? new PluginRegistry();
|
|
428
426
|
this.skillLoader = new SkillLoader();
|
|
429
|
-
// Advanced Intelligence 모듈 초기화
|
|
430
427
|
this.specialistRegistry = new SpecialistRegistry();
|
|
431
428
|
this.toolPlanner = new ToolPlanner();
|
|
432
429
|
this.selfDebugLoop = new SelfDebugLoop();
|
|
433
430
|
this.enableToolPlanning = options.enableToolPlanning !== false;
|
|
434
431
|
this.enableSkillLearning = options.enableSkillLearning !== false;
|
|
435
432
|
this.enableBackgroundAgents = options.enableBackgroundAgents === true;
|
|
436
|
-
// 시스템 프롬프트 추가 (메모리 없이 기본 프롬프트로 시작, init()에서 갱신)
|
|
437
433
|
this.contextManager.addMessage({
|
|
438
434
|
role: "system",
|
|
439
435
|
content: this.config.loop.systemPrompt,
|
|
@@ -464,7 +460,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
464
460
|
if (this.partialInit)
|
|
465
461
|
return;
|
|
466
462
|
this.partialInit = true;
|
|
467
|
-
// ContextBudgetManager 초기화
|
|
468
463
|
this.contextBudgetManager = new ContextBudgetManager({
|
|
469
464
|
totalBudget: this.config.loop.totalTokenBudget,
|
|
470
465
|
enableSummarization: true,
|
|
@@ -473,7 +468,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
473
468
|
const projectPath = this.config.loop.projectPath;
|
|
474
469
|
if (!projectPath)
|
|
475
470
|
return;
|
|
476
|
-
// Session persistence init
|
|
477
471
|
try {
|
|
478
472
|
this.sessionPersistence = new SessionPersistence(undefined, projectPath);
|
|
479
473
|
}
|
|
@@ -481,19 +475,22 @@ export class AgentLoop extends EventEmitter {
|
|
|
481
475
|
this.sessionPersistence = null;
|
|
482
476
|
}
|
|
483
477
|
let yuanMdContent;
|
|
484
|
-
// Memory 로드 (YUAN.md + MemoryManager + PersonaManager — fast file I/O only)
|
|
485
478
|
if (this.enableMemory) {
|
|
486
479
|
try {
|
|
487
|
-
// YUAN.md (raw markdown)
|
|
488
480
|
this.yuanMemory = new YuanMemory(projectPath);
|
|
489
481
|
const memData = await this.yuanMemory.load();
|
|
490
482
|
if (memData) {
|
|
491
483
|
yuanMdContent = memData.raw;
|
|
492
484
|
}
|
|
493
|
-
// MemoryManager (structured learnings)
|
|
494
485
|
this.memoryManager = new MemoryManager(projectPath);
|
|
495
486
|
await this.memoryManager.load();
|
|
496
|
-
|
|
487
|
+
this.judgmentRegistry = new JudgmentRuleRegistry(projectPath);
|
|
488
|
+
// Auto-expand judgment rules from repo capability profile
|
|
489
|
+
try {
|
|
490
|
+
const repoProfile = loadOrScanProfile(projectPath);
|
|
491
|
+
this.judgmentRegistry.autoExpandFromProfile(repoProfile);
|
|
492
|
+
}
|
|
493
|
+
catch { /* non-fatal */ }
|
|
497
494
|
const personaUserId = basename(projectPath) || "default";
|
|
498
495
|
this.personaManager = new PersonaManager({
|
|
499
496
|
userId: personaUserId,
|
|
@@ -503,7 +500,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
503
500
|
await this.personaManager.loadProfile().catch(() => { });
|
|
504
501
|
}
|
|
505
502
|
catch (memErr) {
|
|
506
|
-
// 메모리 로드 실패는 치명적이지 않음 — 경고만 출력
|
|
507
503
|
this.emitEvent({
|
|
508
504
|
kind: "agent:error",
|
|
509
505
|
message: `Memory load failed: ${memErr instanceof Error ? memErr.message : String(memErr)}`,
|
|
@@ -511,7 +507,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
511
507
|
});
|
|
512
508
|
}
|
|
513
509
|
}
|
|
514
|
-
// ExecutionPolicyEngine 로드
|
|
515
510
|
try {
|
|
516
511
|
this.policyEngine = new ExecutionPolicyEngine(projectPath);
|
|
517
512
|
await this.policyEngine.load();
|
|
@@ -520,16 +515,13 @@ export class AgentLoop extends EventEmitter {
|
|
|
520
515
|
this.policyEngine.override(section, values);
|
|
521
516
|
}
|
|
522
517
|
}
|
|
523
|
-
// FailureRecovery에 정책 적용
|
|
524
518
|
const recoveryConfig = this.policyEngine.toFailureRecoveryConfig();
|
|
525
519
|
this.failureRecovery = new FailureRecovery(recoveryConfig);
|
|
526
520
|
}
|
|
527
521
|
catch {
|
|
528
|
-
//
|
|
522
|
+
// Policy load failure — use defaults
|
|
529
523
|
}
|
|
530
|
-
// ImpactAnalyzer 생성
|
|
531
524
|
this.impactAnalyzer = new ImpactAnalyzer({ projectPath });
|
|
532
|
-
// ContinuationEngine 생성 + 체크포인트 복원
|
|
533
525
|
this.continuationEngine = new ContinuationEngine({ projectPath });
|
|
534
526
|
try {
|
|
535
527
|
const latestCheckpoint = await this.continuationEngine.findLatestCheckpoint();
|
|
@@ -539,39 +531,53 @@ export class AgentLoop extends EventEmitter {
|
|
|
539
531
|
role: "system",
|
|
540
532
|
content: continuationPrompt,
|
|
541
533
|
});
|
|
542
|
-
// 복원 후 체크포인트 정리
|
|
543
534
|
await this.continuationEngine.pruneOldCheckpoints();
|
|
544
535
|
}
|
|
545
536
|
}
|
|
546
537
|
catch {
|
|
547
|
-
//
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
538
|
+
// non-fatal
|
|
539
|
+
}
|
|
540
|
+
this._cachedYuanMdContent = yuanMdContent;
|
|
541
|
+
this._cachedProjectStructure = undefined;
|
|
542
|
+
const allTools = [...this.config.loop.tools, ...this.mcpToolDefinitions, SPAWN_SUB_AGENT_TOOL];
|
|
543
|
+
try {
|
|
544
|
+
const envelope = compilePromptEnvelope({
|
|
545
|
+
decision: this.decision,
|
|
546
|
+
promptOptions: {
|
|
547
|
+
projectStructure: undefined, // background에서 analyzeProject() 후 갱신
|
|
548
|
+
yuanMdContent,
|
|
549
|
+
tools: allTools,
|
|
550
|
+
activeToolNames: allTools.map(t => t.name),
|
|
551
|
+
projectPath,
|
|
552
|
+
environment: this.environment,
|
|
553
|
+
},
|
|
554
|
+
runContext: this.pendingRunContext,
|
|
555
|
+
});
|
|
556
|
+
const enhancedPrompt = buildPrompt(envelope);
|
|
557
|
+
this.contextManager.replaceSystemMessage(enhancedPrompt);
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
// Fallback to legacy buildSystemPrompt if PromptRuntime fails
|
|
561
|
+
const enhancedPrompt = buildSystemPrompt({
|
|
562
|
+
projectStructure: undefined,
|
|
563
|
+
yuanMdContent,
|
|
564
|
+
tools: allTools,
|
|
565
|
+
projectPath,
|
|
566
|
+
environment: this.environment,
|
|
567
|
+
});
|
|
568
|
+
this.contextManager.replaceSystemMessage(enhancedPrompt);
|
|
569
|
+
}
|
|
570
|
+
// MemoryManager learnings → pendingRunContext
|
|
560
571
|
if (this.memoryManager) {
|
|
561
572
|
const memory = this.memoryManager.getMemory();
|
|
562
573
|
if (memory.learnings.length > 0 || memory.failedApproaches.length > 0) {
|
|
563
574
|
const memoryContext = this.buildMemoryContext(memory);
|
|
564
575
|
if (memoryContext) {
|
|
565
|
-
this.
|
|
566
|
-
|
|
567
|
-
content: memoryContext,
|
|
568
|
-
});
|
|
576
|
+
this.pendingRunContext.memoryContext = memoryContext;
|
|
577
|
+
this.refreshSystemPrompt();
|
|
569
578
|
}
|
|
570
579
|
}
|
|
571
580
|
}
|
|
572
|
-
// criticalInit 완료 — LLM 호출 가능 상태
|
|
573
|
-
// initialized=true는 backgroundInit() 완료 후 설정됨
|
|
574
|
-
// partialInit은 backgroundInit이 시작될 때까지 유지
|
|
575
581
|
}
|
|
576
582
|
/**
|
|
577
583
|
* TTFT 최적화: LLM 호출을 블로킹하지 않는 백그라운드 초기화.
|
|
@@ -583,8 +589,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
583
589
|
* criticalInit() 완료 후 fire-and-forget으로 실행됨.
|
|
584
590
|
*/
|
|
585
591
|
async backgroundInit() {
|
|
586
|
-
// backgroundInit은 criticalInit이 완료된 후에만 의미 있음
|
|
587
|
-
// initialized=true이면 이미 완료된 것
|
|
588
592
|
if (this.initialized)
|
|
589
593
|
return;
|
|
590
594
|
const projectPath = this.config.loop.projectPath;
|
|
@@ -593,9 +597,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
593
597
|
this.partialInit = false;
|
|
594
598
|
return;
|
|
595
599
|
}
|
|
596
|
-
// partialInit을 false로 — backgroundInit 실행 중이므로 재진입 방어 불필요
|
|
597
600
|
this.partialInit = false;
|
|
598
|
-
// VectorStore — RAG semantic code context (Ollama 느릴 수 있음)
|
|
599
601
|
if (this.enableMemory && this.yuanMemory) {
|
|
600
602
|
try {
|
|
601
603
|
const personaUserId = basename(projectPath) || "default";
|
|
@@ -608,30 +610,21 @@ export class AgentLoop extends EventEmitter {
|
|
|
608
610
|
this.vectorStore.load(),
|
|
609
611
|
new Promise(resolve => setTimeout(resolve, 1_500)),
|
|
610
612
|
]).catch(() => { });
|
|
611
|
-
// Background code indexing — fire and forget
|
|
612
613
|
const vectorStoreRef = this.vectorStore;
|
|
613
614
|
import("./code-indexer.js").then(({ CodeIndexer }) => {
|
|
614
615
|
const indexer = new CodeIndexer({});
|
|
615
616
|
indexer.indexProject(projectPath, vectorStoreRef).catch(() => { });
|
|
616
617
|
}).catch(() => { });
|
|
617
|
-
// 프로젝트 구조 분석 (느림 — file scan)
|
|
618
618
|
const projectStructure = await this.yuanMemory.analyzeProject();
|
|
619
|
-
// analyzeProject 완료 후 시스템 프롬프트 재빌드 (WorldState 아직 없어도 됨)
|
|
620
619
|
const yuanMdContent = (await this.yuanMemory.load().catch(() => null))?.raw;
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
tools: [...this.config.loop.tools, ...this.mcpToolDefinitions, SPAWN_SUB_AGENT_TOOL],
|
|
625
|
-
projectPath,
|
|
626
|
-
environment: this.environment,
|
|
627
|
-
});
|
|
628
|
-
this.contextManager.replaceSystemMessage(updatedPrompt);
|
|
620
|
+
this._cachedProjectStructure = projectStructure;
|
|
621
|
+
this._cachedYuanMdContent = yuanMdContent;
|
|
622
|
+
this.refreshSystemPrompt();
|
|
629
623
|
}
|
|
630
624
|
catch {
|
|
631
625
|
// non-fatal
|
|
632
626
|
}
|
|
633
627
|
}
|
|
634
|
-
// WorldState 수집 → system prompt에 주입
|
|
635
628
|
try {
|
|
636
629
|
const worldStateCollector = new WorldStateCollector({
|
|
637
630
|
projectPath,
|
|
@@ -639,22 +632,17 @@ export class AgentLoop extends EventEmitter {
|
|
|
639
632
|
skipTest: true,
|
|
640
633
|
});
|
|
641
634
|
this.worldState = await worldStateCollector.collect();
|
|
642
|
-
// WorldState 섹션 시스템 프롬프트에 추가
|
|
643
635
|
if (this.worldState) {
|
|
644
636
|
const collector = new WorldStateCollector({ projectPath });
|
|
645
|
-
const worldStateSection =
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
const sysMsg = currentMsgs.find((m) => m.role === "system");
|
|
649
|
-
if (sysMsg && !String(sysMsg.content).includes("## Current Environment")) {
|
|
650
|
-
this.contextManager.replaceSystemMessage(String(sysMsg.content) + worldStateSection);
|
|
651
|
-
}
|
|
637
|
+
const worldStateSection = collector.formatForPrompt(this.worldState);
|
|
638
|
+
this.pendingRunContext.worldStateSection = worldStateSection;
|
|
639
|
+
this.refreshSystemPrompt();
|
|
652
640
|
}
|
|
653
641
|
}
|
|
654
642
|
catch {
|
|
655
|
-
//
|
|
643
|
+
// non-fatal
|
|
656
644
|
}
|
|
657
|
-
// CodeOrchestrator
|
|
645
|
+
// CodeOrchestrator — append directly (not yet in runContext schema)
|
|
658
646
|
try {
|
|
659
647
|
const { codeOrchestrator } = await import('./code-orchestrator.js');
|
|
660
648
|
const codeCtx = await codeOrchestrator.getContextForLLM(projectPath ?? '');
|
|
@@ -667,7 +655,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
667
655
|
}
|
|
668
656
|
}
|
|
669
657
|
catch { /* non-fatal */ }
|
|
670
|
-
// Initialize World Model
|
|
671
658
|
if (this.worldState && projectPath) {
|
|
672
659
|
try {
|
|
673
660
|
this.transitionModel = new TransitionModel();
|
|
@@ -675,11 +662,8 @@ export class AgentLoop extends EventEmitter {
|
|
|
675
662
|
this.simulationEngine = new SimulationEngine(this.transitionModel, this.worldModel);
|
|
676
663
|
this.stateUpdater = new StateUpdater(this.worldModel, projectPath);
|
|
677
664
|
}
|
|
678
|
-
catch {
|
|
679
|
-
// World Model initialization failure is non-fatal
|
|
680
|
-
}
|
|
665
|
+
catch { /* non-fatal */ }
|
|
681
666
|
}
|
|
682
|
-
// Capture last known good git commit for FailureRecovery rollback
|
|
683
667
|
try {
|
|
684
668
|
const headHash = execSync("git rev-parse HEAD", {
|
|
685
669
|
cwd: projectPath,
|
|
@@ -691,9 +675,8 @@ export class AgentLoop extends EventEmitter {
|
|
|
691
675
|
}
|
|
692
676
|
}
|
|
693
677
|
catch {
|
|
694
|
-
//
|
|
678
|
+
// non-fatal — FailureRecovery will use file-level rollback
|
|
695
679
|
}
|
|
696
|
-
// MCP 클라이언트 연결 (connectAll은 네트워크 I/O라 느릴 수 있음)
|
|
697
680
|
{
|
|
698
681
|
let mergedMCPConfigs = [...this.mcpServerConfigs];
|
|
699
682
|
try {
|
|
@@ -732,9 +715,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
732
715
|
}
|
|
733
716
|
}
|
|
734
717
|
}
|
|
735
|
-
// ReflexionEngine 생성
|
|
736
718
|
this.reflexionEngine = new ReflexionEngine({ projectPath });
|
|
737
|
-
// SelfReflection 생성 (6D deep verify + quick verify)
|
|
738
719
|
if (this.enableSelfReflection) {
|
|
739
720
|
const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
740
721
|
this.selfReflection = new SelfReflection(sessionId, {
|
|
@@ -749,12 +730,9 @@ export class AgentLoop extends EventEmitter {
|
|
|
749
730
|
const memory = await this.memoryManager.load();
|
|
750
731
|
this.selfReflection.loadFromMemory(memory);
|
|
751
732
|
}
|
|
752
|
-
catch {
|
|
753
|
-
// 학습 복원 실패는 치명적이지 않음
|
|
754
|
-
}
|
|
733
|
+
catch { /* non-fatal */ }
|
|
755
734
|
}
|
|
756
735
|
}
|
|
757
|
-
// DebateOrchestrator 생성 (complex/massive 태스크에서 multi-agent debate)
|
|
758
736
|
if (this.enableDebate && projectPath) {
|
|
759
737
|
this.debateOrchestrator = new DebateOrchestrator({
|
|
760
738
|
projectPath,
|
|
@@ -767,13 +745,31 @@ export class AgentLoop extends EventEmitter {
|
|
|
767
745
|
totalTokenBudget: Math.floor(this.config.loop.totalTokenBudget * 0.3),
|
|
768
746
|
});
|
|
769
747
|
}
|
|
770
|
-
// Self-model weakness context injection
|
|
771
748
|
const weaknessCtx = getSelfWeaknessContext(this.capabilitySelfModel);
|
|
772
749
|
if (weaknessCtx) {
|
|
773
|
-
this.
|
|
750
|
+
this.pendingRunContext.weaknessContext = weaknessCtx;
|
|
751
|
+
this.refreshSystemPrompt();
|
|
752
|
+
}
|
|
753
|
+
// ModelWeaknessTracker — learn from repeated validator blocks
|
|
754
|
+
if (projectPath) {
|
|
755
|
+
try {
|
|
756
|
+
const modelId = this.config.byok.model ?? this.config.byok.provider;
|
|
757
|
+
this.weaknessTracker = new ModelWeaknessTracker(projectPath, modelId);
|
|
758
|
+
const hints = this.weaknessTracker.getPreventiveHints();
|
|
759
|
+
if (hints.length > 0) {
|
|
760
|
+
const existing = this.pendingRunContext.weaknessContext ?? "";
|
|
761
|
+
this.pendingRunContext.weaknessContext =
|
|
762
|
+
(existing ? existing + "\n" : "") +
|
|
763
|
+
"[Model Weakness Prevention]\n" + hints.map(h => `- ${h}`).join("\n");
|
|
764
|
+
this.refreshSystemPrompt();
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
this.weaknessTracker = null;
|
|
769
|
+
}
|
|
774
770
|
}
|
|
775
|
-
|
|
776
|
-
if (
|
|
771
|
+
const shouldLearnSkills = this.decision.core.skillActivation.enableSkillLearning;
|
|
772
|
+
if (shouldLearnSkills && projectPath) {
|
|
777
773
|
try {
|
|
778
774
|
this.skillLearner = new SkillLearner(projectPath);
|
|
779
775
|
await this.skillLearner.init();
|
|
@@ -782,7 +778,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
782
778
|
this.skillLearner = null;
|
|
783
779
|
}
|
|
784
780
|
}
|
|
785
|
-
// Phase 4: FailureSignatureMemory, PlaybookLibrary, ProjectExecutive, StallDetector
|
|
786
781
|
if (projectPath) {
|
|
787
782
|
try {
|
|
788
783
|
this.failureSigMemory = new FailureSignatureMemory({ projectPath });
|
|
@@ -796,21 +791,15 @@ export class AgentLoop extends EventEmitter {
|
|
|
796
791
|
this.metaLearningCollector = new MetaLearningCollector({ projectPath });
|
|
797
792
|
this.trustEconomics = new TrustEconomics({ projectPath });
|
|
798
793
|
}
|
|
799
|
-
catch {
|
|
800
|
-
// Phase 4 init failure is non-fatal
|
|
801
|
-
}
|
|
794
|
+
catch { /* non-fatal */ }
|
|
802
795
|
}
|
|
803
|
-
// Phase 5: StrategyLearner + SkillRegistry
|
|
804
796
|
try {
|
|
805
797
|
this.strategyLearner = new StrategyLearner();
|
|
806
798
|
this.skillRegistry = new SkillRegistry();
|
|
807
799
|
this.strategyLearner.on("event", (ev) => this.emitEvent(ev));
|
|
808
800
|
this.skillRegistry.on("event", (ev) => this.emitEvent(ev));
|
|
809
801
|
}
|
|
810
|
-
catch {
|
|
811
|
-
// Phase 5 init failure is non-fatal
|
|
812
|
-
}
|
|
813
|
-
// Phase 5 extended: TracePatternExtractor, MetaLearningEngine, ToolSynthesizer
|
|
802
|
+
catch { /* non-fatal */ }
|
|
814
803
|
try {
|
|
815
804
|
this.tracePatternExtractor = new TracePatternExtractor();
|
|
816
805
|
this.metaLearningEngine = new MetaLearningEngine({
|
|
@@ -822,10 +811,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
822
811
|
this.metaLearningEngine.on("event", (ev) => this.emitEvent(ev));
|
|
823
812
|
this.toolSynthesizer.on("event", (ev) => this.emitEvent(ev));
|
|
824
813
|
}
|
|
825
|
-
catch {
|
|
826
|
-
// Phase 5 extended init failure is non-fatal
|
|
827
|
-
}
|
|
828
|
-
// Phase 6: BudgetGovernorV2, CapabilityGraph, CapabilitySelfModel, StrategyMarket
|
|
814
|
+
catch { /* non-fatal */ }
|
|
829
815
|
try {
|
|
830
816
|
this.budgetGovernorV2 = new BudgetGovernorV2({ taskBudget: this.config.loop.totalTokenBudget || 200_000 });
|
|
831
817
|
this.capabilityGraph = new CapabilityGraph();
|
|
@@ -839,12 +825,10 @@ export class AgentLoop extends EventEmitter {
|
|
|
839
825
|
registerToolsInGraph(this.capabilityGraph, toolNames);
|
|
840
826
|
}
|
|
841
827
|
catch { /* non-fatal */ }
|
|
842
|
-
// HierarchicalPlanner 생성
|
|
843
828
|
this.planner = new HierarchicalPlanner({ projectPath });
|
|
844
829
|
if (this.skillLearner) {
|
|
845
830
|
this.planner.setSkillLearner(this.skillLearner);
|
|
846
831
|
}
|
|
847
|
-
// Initialize Proactive Replanning (requires planner + impactAnalyzer + worldModel)
|
|
848
832
|
try {
|
|
849
833
|
if (this.impactAnalyzer && this.worldModel && this.simulationEngine && this.transitionModel) {
|
|
850
834
|
this.milestoneChecker = new MilestoneChecker();
|
|
@@ -853,9 +837,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
853
837
|
this.replanningEngine = new ReplanningEngine(this.planner, this.planEvaluator, this.riskEstimator, this.milestoneChecker);
|
|
854
838
|
}
|
|
855
839
|
}
|
|
856
|
-
catch {
|
|
857
|
-
// Proactive replanning initialization failure is non-fatal
|
|
858
|
-
}
|
|
840
|
+
catch { /* non-fatal */ }
|
|
859
841
|
if (this.skillLearner) {
|
|
860
842
|
const learnedSkills = this.skillLearner.getAllSkills();
|
|
861
843
|
if (learnedSkills.length > 0) {
|
|
@@ -863,14 +845,11 @@ export class AgentLoop extends EventEmitter {
|
|
|
863
845
|
.filter((s) => s.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD)
|
|
864
846
|
.map((s) => s.id);
|
|
865
847
|
if (skillNames.length > 0) {
|
|
866
|
-
this.
|
|
867
|
-
|
|
868
|
-
content: `[Learned Skills: ${skillNames.join(", ")}] — Auto-activate on matching error patterns.`,
|
|
869
|
-
});
|
|
848
|
+
this.pendingRunContext.learnedSkills = `[Learned Skills: ${skillNames.join(", ")}] — Auto-activate on matching error patterns.`;
|
|
849
|
+
this.refreshSystemPrompt();
|
|
870
850
|
}
|
|
871
851
|
}
|
|
872
852
|
}
|
|
873
|
-
// RepoKnowledgeGraph 초기화 (코드 구조 그래프 — 비동기 빌드)
|
|
874
853
|
try {
|
|
875
854
|
this.repoGraph = new RepoKnowledgeGraph(projectPath);
|
|
876
855
|
this.repoGraph.buildFromProject(projectPath).catch(() => { });
|
|
@@ -878,8 +857,8 @@ export class AgentLoop extends EventEmitter {
|
|
|
878
857
|
catch {
|
|
879
858
|
this.repoGraph = null;
|
|
880
859
|
}
|
|
881
|
-
|
|
882
|
-
if (
|
|
860
|
+
const shouldEnableBgAgents = this.decision.core.subAgentPlan.enabled;
|
|
861
|
+
if (shouldEnableBgAgents && projectPath) {
|
|
883
862
|
try {
|
|
884
863
|
this.backgroundAgentManager = new BackgroundAgentManager();
|
|
885
864
|
this.backgroundAgentManager.createDefaults(projectPath);
|
|
@@ -903,18 +882,14 @@ export class AgentLoop extends EventEmitter {
|
|
|
903
882
|
this.backgroundAgentManager = null;
|
|
904
883
|
}
|
|
905
884
|
}
|
|
906
|
-
// Inject active plugin skills into system prompt (lazy: names only)
|
|
907
885
|
const activeSkills = this.pluginRegistry.getAllSkills();
|
|
908
886
|
if (activeSkills.length > 0) {
|
|
909
887
|
const skillSummary = activeSkills
|
|
910
888
|
.map((s) => `${s.pluginId}/${s.skill.name}`)
|
|
911
889
|
.join(", ");
|
|
912
|
-
this.
|
|
913
|
-
|
|
914
|
-
content: `[Plugins: ${skillSummary}] — Skills auto-activate on matching files/errors.`,
|
|
915
|
-
});
|
|
890
|
+
this.pendingRunContext.pluginSkills = `[Plugins: ${skillSummary}] — Skills auto-activate on matching files/errors.`;
|
|
891
|
+
this.refreshSystemPrompt();
|
|
916
892
|
}
|
|
917
|
-
// ContinuousReflection 생성 (1분 간격 체크포인트 + 자기검증 + 컨텍스트 모니터)
|
|
918
893
|
this.continuousReflection = new ContinuousReflection({
|
|
919
894
|
getState: () => this.getStateSnapshot(),
|
|
920
895
|
checkpoint: async (state, _emergency) => {
|
|
@@ -965,7 +940,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
965
940
|
}
|
|
966
941
|
},
|
|
967
942
|
});
|
|
968
|
-
// Wire ContinuousReflection events to agent loop
|
|
969
943
|
this.continuousReflection.on("reflection:feedback", (feedback) => {
|
|
970
944
|
this.contextManager.addMessage({
|
|
971
945
|
role: "system",
|
|
@@ -981,7 +955,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
981
955
|
this.continuousReflection.on("reflection:context_overflow", () => {
|
|
982
956
|
void this.handleSoftContextOverflow();
|
|
983
957
|
});
|
|
984
|
-
// Mark fully initialized
|
|
985
958
|
this.initialized = true;
|
|
986
959
|
this.partialInit = false;
|
|
987
960
|
}
|
|
@@ -1014,6 +987,35 @@ export class AgentLoop extends EventEmitter {
|
|
|
1014
987
|
}
|
|
1015
988
|
return parts.length > 0 ? parts.join("\n") : null;
|
|
1016
989
|
}
|
|
990
|
+
/**
|
|
991
|
+
* 3-Layer Prompt 재빌드: pendingRunContext + cached state → PromptRuntime → PromptBuilder.
|
|
992
|
+
* backgroundInit()에서 부분 컨텍스트가 채워질 때마다 호출.
|
|
993
|
+
*/
|
|
994
|
+
refreshSystemPrompt() {
|
|
995
|
+
if (!this.initialized && !this.partialInit)
|
|
996
|
+
return;
|
|
997
|
+
const projectPath = this.config.loop.projectPath;
|
|
998
|
+
const allTools = [...this.config.loop.tools, ...this.mcpToolDefinitions, SPAWN_SUB_AGENT_TOOL];
|
|
999
|
+
try {
|
|
1000
|
+
const envelope = compilePromptEnvelope({
|
|
1001
|
+
decision: this.decision,
|
|
1002
|
+
promptOptions: {
|
|
1003
|
+
projectStructure: this._cachedProjectStructure,
|
|
1004
|
+
yuanMdContent: this._cachedYuanMdContent,
|
|
1005
|
+
tools: allTools,
|
|
1006
|
+
activeToolNames: allTools.map(t => t.name),
|
|
1007
|
+
projectPath,
|
|
1008
|
+
environment: this.environment,
|
|
1009
|
+
},
|
|
1010
|
+
runContext: this.pendingRunContext,
|
|
1011
|
+
});
|
|
1012
|
+
const prompt = buildPrompt(envelope);
|
|
1013
|
+
this.contextManager.replaceSystemMessage(prompt);
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
// If PromptRuntime fails, leave existing system message as-is
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1017
1019
|
/**
|
|
1018
1020
|
* 에이전트 루프를 실행.
|
|
1019
1021
|
* 첫 호출 시 자동으로 Memory와 프로젝트 컨텍스트를 로드한다.
|
|
@@ -1026,7 +1028,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
1026
1028
|
dlog("AGENT-LOOP", `run() called`, { goal: userMessage.slice(0, 120), resumedFromSession: this.resumedFromSession });
|
|
1027
1029
|
this.reasoningAggregator.reset();
|
|
1028
1030
|
this.reasoningTree.reset();
|
|
1029
|
-
// Capture before reset so session snapshot gets accurate file list
|
|
1030
1031
|
const prevChangedFiles = [...this.changedFiles];
|
|
1031
1032
|
if (!this.resumedFromSession) {
|
|
1032
1033
|
this.changedFiles = [];
|
|
@@ -1043,69 +1044,58 @@ export class AgentLoop extends EventEmitter {
|
|
|
1043
1044
|
total: 0,
|
|
1044
1045
|
};
|
|
1045
1046
|
this.impactHintInjected = false;
|
|
1046
|
-
|
|
1047
|
+
this.pendingRunContext = {};
|
|
1048
|
+
this.decision = DEFAULT_DECISION;
|
|
1049
|
+
this.toolUsageCounter = { reads: 0, edits: 0, shells: 0, tests: 0, searches: 0, webLookups: 0, sameFileEdits: new Map() };
|
|
1047
1050
|
this.iterationTsFilesModified = [];
|
|
1048
1051
|
this.tscRanLastIteration = false;
|
|
1049
|
-
// Task 1: reset context summarization guard per run
|
|
1050
1052
|
this._contextSummarizationDone = false;
|
|
1051
1053
|
this._unfulfilledContinuations = 0;
|
|
1052
|
-
// aggressive recovery fields removed
|
|
1053
1054
|
}
|
|
1054
|
-
// Reset resumedFromSession flag after consuming it — prevents stale state
|
|
1055
|
-
// from leaking into subsequent runs when the same AgentLoop instance is reused.
|
|
1056
1055
|
this.resumedFromSession = false;
|
|
1057
1056
|
this.checkpointSaved = false;
|
|
1058
1057
|
this.failureRecovery.reset();
|
|
1058
|
+
this.patchJournal?.reset();
|
|
1059
|
+
this.patchScopeController?.reset();
|
|
1059
1060
|
this.costOptimizer.reset();
|
|
1060
1061
|
this.tokenBudgetManager.reset();
|
|
1061
1062
|
const runStartTime = Date.now();
|
|
1062
|
-
// 즉시 start 이벤트 emit — init 전에 TUI 타이머/상태 시작
|
|
1063
1063
|
dlog("AGENT-LOOP", `emitting agent:start, sessionId=${this.sessionId}`);
|
|
1064
1064
|
this.emitEvent({ kind: "agent:start", goal: userMessage });
|
|
1065
|
-
// 첫 실행 시 메모리/프로젝트 컨텍스트 자동 로드
|
|
1066
|
-
// criticalInit은 최대 1초만 블로킹 — LLM 호출 TTFT 최소화
|
|
1067
|
-
// backgroundInit은 fire-and-forget — LLM 호출을 블로킹하지 않음
|
|
1068
1065
|
await Promise.race([
|
|
1069
1066
|
this.criticalInit(),
|
|
1070
1067
|
new Promise(resolve => setTimeout(resolve, 1_000)),
|
|
1071
1068
|
]);
|
|
1072
|
-
// If criticalInit timed out (partialInit still true), allow retry on next run
|
|
1073
1069
|
if (this.partialInit && !this.initialized) {
|
|
1074
1070
|
this.partialInit = false;
|
|
1075
1071
|
}
|
|
1076
|
-
// Background init — does NOT block LLM call
|
|
1077
1072
|
this.backgroundInit().catch(() => { });
|
|
1078
|
-
// Always generate a fresh sessionId per run — prevents BudgetGovernorV2
|
|
1079
|
-
// from accumulating exhausted task budget across multiple runs on the same instance.
|
|
1080
1073
|
this.sessionId = randomUUID();
|
|
1081
|
-
//
|
|
1074
|
+
// Initialize patch transaction journal for atomic rollback
|
|
1075
|
+
if (this.config.loop.projectPath) {
|
|
1076
|
+
this.patchJournal = new PatchTransactionJournal(this.config.loop.projectPath, this.sessionId);
|
|
1077
|
+
}
|
|
1082
1078
|
try {
|
|
1083
1079
|
this.budgetGovernorV2?.startTask(this.sessionId ?? "default");
|
|
1084
1080
|
initMarketPlaybooks(this.strategyMarket);
|
|
1085
1081
|
}
|
|
1086
1082
|
catch { /* non-fatal */ }
|
|
1087
|
-
// Initialize trace recorder (once per run)
|
|
1088
1083
|
if (!this.traceRecorder) {
|
|
1089
1084
|
this.traceRecorder = new TraceRecorder(this.sessionId);
|
|
1090
1085
|
}
|
|
1091
|
-
// Initialize arch summarizer (once per run)
|
|
1092
1086
|
if (!this.archSummarizer && this.config.loop.projectPath) {
|
|
1093
1087
|
this.archSummarizer = new ArchSummarizer(this.config.loop.projectPath);
|
|
1094
|
-
// Trigger refresh in background if dirty
|
|
1095
1088
|
this.archSummarizer.getSummary().catch(() => { });
|
|
1096
1089
|
}
|
|
1097
|
-
// Phase 4: PlaybookLibrary — inject hint into system context based on goal
|
|
1098
1090
|
if (this.playbookLibrary) {
|
|
1099
1091
|
try {
|
|
1100
1092
|
const playbook = this.playbookLibrary.query(userMessage);
|
|
1101
1093
|
if (playbook) {
|
|
1102
|
-
this.
|
|
1103
|
-
|
|
1104
|
-
content: `[Playbook: ${playbook.taskType} v${playbook.version}] ` +
|
|
1094
|
+
this.pendingRunContext.playbookHint =
|
|
1095
|
+
`[Playbook: ${playbook.taskType} v${playbook.version}] ` +
|
|
1105
1096
|
`Phase order: ${playbook.phaseOrder.join(" → ")}. ` +
|
|
1106
1097
|
`Stop conditions: ${playbook.stopConditions.join(", ")}. ` +
|
|
1107
|
-
`Evidence required: ${playbook.evidenceRequirements.join(", ")}
|
|
1108
|
-
});
|
|
1098
|
+
`Evidence required: ${playbook.evidenceRequirements.join(", ")}.`;
|
|
1109
1099
|
}
|
|
1110
1100
|
}
|
|
1111
1101
|
catch { /* non-fatal */ }
|
|
@@ -1137,7 +1127,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
1137
1127
|
changedFiles: prevChangedFiles,
|
|
1138
1128
|
});
|
|
1139
1129
|
}
|
|
1140
|
-
// 사용자 입력 검증 (prompt injection 방어)
|
|
1141
1130
|
const inputValidation = this.promptDefense.validateUserInput(userMessage);
|
|
1142
1131
|
if (inputValidation.injectionDetected && (inputValidation.severity === "critical" || inputValidation.severity === "high")) {
|
|
1143
1132
|
this.emitEvent({
|
|
@@ -1146,18 +1135,14 @@ export class AgentLoop extends EventEmitter {
|
|
|
1146
1135
|
retryable: false,
|
|
1147
1136
|
});
|
|
1148
1137
|
}
|
|
1149
|
-
// 사용자 메시지 추가
|
|
1150
1138
|
this.contextManager.addMessage({
|
|
1151
1139
|
role: "user",
|
|
1152
1140
|
content: userMessage,
|
|
1153
1141
|
});
|
|
1154
|
-
// PersonaManager — 유저 메시지로 커뮤니케이션 스타일 학습
|
|
1155
1142
|
this.lastUserMessage = userMessage;
|
|
1156
1143
|
if (this.personaManager) {
|
|
1157
1144
|
this.personaManager.analyzeUserMessage(userMessage);
|
|
1158
1145
|
}
|
|
1159
|
-
// Vision Intent Detection — user message
|
|
1160
|
-
// If the user signals they want to look at an image, auto-read it and inject as vision.
|
|
1161
1146
|
try {
|
|
1162
1147
|
const visionIntent = this.visionIntentDetector.detect(userMessage);
|
|
1163
1148
|
if (visionIntent && visionIntent.confidence >= 0.5) {
|
|
@@ -1186,18 +1171,14 @@ export class AgentLoop extends EventEmitter {
|
|
|
1186
1171
|
}
|
|
1187
1172
|
}
|
|
1188
1173
|
}
|
|
1189
|
-
catch {
|
|
1190
|
-
// Vision intent detection is non-fatal; continue normally
|
|
1191
|
-
}
|
|
1174
|
+
catch { /* non-fatal */ }
|
|
1192
1175
|
try {
|
|
1193
|
-
// Persona injection — 유저 선호도/언어/스타일 어댑테이션을 시스템 메시지로 주입
|
|
1194
1176
|
if (this.personaManager) {
|
|
1195
1177
|
const personaSection = this.personaManager.buildPersonaPrompt();
|
|
1196
1178
|
if (personaSection) {
|
|
1197
|
-
this.
|
|
1179
|
+
this.pendingRunContext.personaSection = personaSection;
|
|
1198
1180
|
}
|
|
1199
1181
|
}
|
|
1200
|
-
// MemoryManager.getRelevant() — 현재 태스크와 관련된 conventions/patterns/warnings 주입
|
|
1201
1182
|
if (this.memoryManager) {
|
|
1202
1183
|
const relevant = this.memoryManager.getRelevant(userMessage);
|
|
1203
1184
|
const parts = [];
|
|
@@ -1211,13 +1192,9 @@ export class AgentLoop extends EventEmitter {
|
|
|
1211
1192
|
parts.push(`## Relevant Code Patterns\n${relevant.patterns.slice(0, 3).map((p) => `- **${p.name}**: ${p.description}`).join("\n")}`);
|
|
1212
1193
|
}
|
|
1213
1194
|
if (parts.length > 0) {
|
|
1214
|
-
this.
|
|
1215
|
-
role: "system",
|
|
1216
|
-
content: `[Task Memory]\n${parts.join("\n\n")}`,
|
|
1217
|
-
});
|
|
1195
|
+
this.pendingRunContext.taskMemory = `[Task Memory]\n${parts.join("\n\n")}`;
|
|
1218
1196
|
}
|
|
1219
1197
|
}
|
|
1220
|
-
// VectorStore RAG — 태스크와 의미적으로 유사한 코드 컨텍스트 검색·주입
|
|
1221
1198
|
if (this.vectorStore) {
|
|
1222
1199
|
try {
|
|
1223
1200
|
const hits = await this.vectorStore.search(userMessage, 3, 0.2);
|
|
@@ -1225,70 +1202,27 @@ export class AgentLoop extends EventEmitter {
|
|
|
1225
1202
|
const ragCtx = hits
|
|
1226
1203
|
.map((h) => `**${h.id}** (relevance: ${(h.similarity * 100).toFixed(0)}%)\n${h.text.slice(0, 400)}`)
|
|
1227
1204
|
.join("\n\n---\n\n");
|
|
1228
|
-
this.
|
|
1229
|
-
role: "system",
|
|
1230
|
-
content: `[RAG Context — semantically relevant code snippets]\n${ragCtx}`,
|
|
1231
|
-
});
|
|
1205
|
+
this.pendingRunContext.ragContext = `[RAG Context — semantically relevant code snippets]\n${ragCtx}`;
|
|
1232
1206
|
}
|
|
1233
1207
|
}
|
|
1234
|
-
catch {
|
|
1235
|
-
// VectorStore search failure is non-fatal
|
|
1236
|
-
}
|
|
1208
|
+
catch { /* non-fatal */ }
|
|
1237
1209
|
}
|
|
1238
|
-
// Reflexion: 과거 실행에서 배운 가이던스 주입
|
|
1239
1210
|
if (this.reflexionEngine) {
|
|
1240
1211
|
try {
|
|
1241
1212
|
const guidance = await this.reflexionEngine.getGuidance(userMessage);
|
|
1242
|
-
// 가이던스 유효성 검증: 빈 전략이나 매우 낮은 confidence 필터링
|
|
1243
1213
|
const validStrategies = guidance.relevantStrategies.filter((s) => s.strategy && s.strategy.length > 5 && s.confidence > 0.1);
|
|
1244
1214
|
if (validStrategies.length > 0 || guidance.recentFailures.length > 0) {
|
|
1245
1215
|
const filteredGuidance = { ...guidance, relevantStrategies: validStrategies };
|
|
1246
1216
|
const guidancePrompt = this.reflexionEngine.formatForSystemPrompt(filteredGuidance);
|
|
1247
|
-
this.
|
|
1248
|
-
role: "system",
|
|
1249
|
-
content: guidancePrompt,
|
|
1250
|
-
});
|
|
1217
|
+
this.pendingRunContext.reflexionGuidance = guidancePrompt;
|
|
1251
1218
|
}
|
|
1252
1219
|
}
|
|
1253
|
-
catch {
|
|
1254
|
-
// guidance 로드 실패는 치명적이지 않음
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
// Task 분류 → 시스템 프롬프트에 tool sequence hint 주입
|
|
1258
|
-
const classification = this.taskClassifier.classify(userMessage);
|
|
1259
|
-
if (classification.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD) {
|
|
1260
|
-
const classificationHint = this.taskClassifier.formatForSystemPrompt(classification);
|
|
1261
|
-
this.contextManager.addMessage({
|
|
1262
|
-
role: "system",
|
|
1263
|
-
content: classificationHint,
|
|
1264
|
-
});
|
|
1265
|
-
}
|
|
1266
|
-
// Specialist routing: 태스크 타입에 맞는 전문 에이전트 설정 주입
|
|
1267
|
-
if (classification.specialistDomain) {
|
|
1268
|
-
const specialistMatch = this.specialistRegistry.findSpecialist(classification.specialistDomain);
|
|
1269
|
-
if (specialistMatch && specialistMatch.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD) {
|
|
1270
|
-
this.contextManager.addMessage({
|
|
1271
|
-
role: "system",
|
|
1272
|
-
content: `[Specialist: ${specialistMatch.specialist.name}] ${specialistMatch.specialist.systemPrompt.slice(0, 500)}`,
|
|
1273
|
-
});
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
// Tool Planning: 태스크 타입에 맞는 도구 실행 계획 힌트 주입
|
|
1277
|
-
if (this.enableToolPlanning && classification.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD) {
|
|
1278
|
-
const planContext = {
|
|
1279
|
-
userMessage,
|
|
1280
|
-
};
|
|
1281
|
-
this.currentToolPlan = this.toolPlanner.planForTask(classification.type, planContext);
|
|
1282
|
-
this.executedToolNames = [];
|
|
1283
|
-
if (this.currentToolPlan.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD) {
|
|
1284
|
-
const planHint = this.toolPlanner.formatPlanHint(this.currentToolPlan);
|
|
1285
|
-
this.contextManager.addMessage({
|
|
1286
|
-
role: "system",
|
|
1287
|
-
content: planHint,
|
|
1288
|
-
});
|
|
1289
|
-
}
|
|
1220
|
+
catch { /* non-fatal */ }
|
|
1290
1221
|
}
|
|
1291
|
-
|
|
1222
|
+
this.refreshSystemPrompt();
|
|
1223
|
+
// Legacy task classification block removed — Decision Engine is now always present.
|
|
1224
|
+
// Task classification, specialist routing, and tool planning are handled via
|
|
1225
|
+
// decision.core.skillActivation in the Decision-based overrides section below.
|
|
1292
1226
|
if (this.config.loop.projectPath) {
|
|
1293
1227
|
try {
|
|
1294
1228
|
const renameMatch = userMessage.match(/\brename\s+[`'"]?(\w[\w.]*)[`'"]?\s+to\s+[`'"]?(\w[\w.]*)[`'"]/i);
|
|
@@ -1316,7 +1250,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
1316
1250
|
else if (moveMatch) {
|
|
1317
1251
|
const [, symbolOrFile, destination] = moveMatch;
|
|
1318
1252
|
const refactor = new CrossFileRefactor(this.config.loop.projectPath);
|
|
1319
|
-
// Try move as symbol move (source heuristic: look for a file with that name)
|
|
1320
1253
|
const preview = await refactor.moveSymbol(symbolOrFile, symbolOrFile, destination);
|
|
1321
1254
|
if (preview.totalChanges > 0) {
|
|
1322
1255
|
const affectedList = preview.affectedFiles
|
|
@@ -1335,13 +1268,123 @@ export class AgentLoop extends EventEmitter {
|
|
|
1335
1268
|
}
|
|
1336
1269
|
}
|
|
1337
1270
|
}
|
|
1338
|
-
catch {
|
|
1339
|
-
|
|
1271
|
+
catch { /* non-fatal */ }
|
|
1272
|
+
}
|
|
1273
|
+
try {
|
|
1274
|
+
let projectCtx = this.worldState
|
|
1275
|
+
? worldStateToProjectContext(this.worldState)
|
|
1276
|
+
: undefined;
|
|
1277
|
+
// Codebase complexity stats are enriched by ExecutionEngine when it calls agentDecide
|
|
1278
|
+
// AgentLoop uses worldState only; CodebaseContext lives in ExecutionEngine
|
|
1279
|
+
this.decision = agentDecide({
|
|
1280
|
+
message: userMessage,
|
|
1281
|
+
projectContext: projectCtx,
|
|
1282
|
+
prevDecision: this.prevDecision ?? undefined,
|
|
1283
|
+
});
|
|
1284
|
+
this.emitEvent({
|
|
1285
|
+
kind: "agent:decision",
|
|
1286
|
+
decision: {
|
|
1287
|
+
intent: this.decision.core.reasoning.intent,
|
|
1288
|
+
complexity: this.decision.core.reasoning.complexity,
|
|
1289
|
+
taskStage: this.decision.core.reasoning.taskStage,
|
|
1290
|
+
planRequired: this.decision.core.planRequired,
|
|
1291
|
+
nextAction: this.decision.core.nextAction,
|
|
1292
|
+
},
|
|
1293
|
+
});
|
|
1294
|
+
this.emitEvent({
|
|
1295
|
+
kind: "agent:interaction_mode",
|
|
1296
|
+
mode: this.decision.core.interactionMode,
|
|
1297
|
+
});
|
|
1298
|
+
if (this.decision.core.nextAction === "ask_user" && this.decision.core.clarification) {
|
|
1299
|
+
const clar = this.decision.core.clarification;
|
|
1300
|
+
const clarMsg = `I need some clarification before proceeding:\n\n${clar.reason}` +
|
|
1301
|
+
(clar.missingFields.length > 0 ? `\n\nMissing: ${clar.missingFields.join(", ")}` : "") +
|
|
1302
|
+
(clar.allowProceedWithAssumptions ? "\n\n(I can proceed with assumptions if you prefer.)" : "");
|
|
1303
|
+
this.emitEvent({ kind: "agent:completed", summary: clarMsg, filesChanged: [] });
|
|
1304
|
+
return { reason: "NEEDS_CLARIFICATION", summary: clarMsg };
|
|
1305
|
+
}
|
|
1306
|
+
if (this.decision.core.nextAction === "blocked_external") {
|
|
1307
|
+
const blockedMsg = "This task appears to be blocked by an external dependency. Please resolve the blocking issue and retry.";
|
|
1308
|
+
this.emitEvent({ kind: "agent:completed", summary: blockedMsg, filesChanged: [] });
|
|
1309
|
+
return { reason: "BLOCKED_EXTERNAL", summary: blockedMsg };
|
|
1310
|
+
}
|
|
1311
|
+
this.currentComplexity = this.decision.core.reasoning.complexity;
|
|
1312
|
+
const decMode = this.decision.core.interactionMode;
|
|
1313
|
+
const decVd = this.decision.core.verifyDepth;
|
|
1314
|
+
if (decMode === "CHAT") {
|
|
1315
|
+
this.overheadGovernor.overrideConfig({
|
|
1316
|
+
autoTsc: "OFF",
|
|
1317
|
+
debate: "OFF",
|
|
1318
|
+
deepVerify: "OFF",
|
|
1319
|
+
quickVerify: "OFF",
|
|
1320
|
+
qaPipeline: "OFF",
|
|
1321
|
+
llmFixer: "OFF",
|
|
1322
|
+
summarize: "OFF",
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
else if (decMode === "AGENT" && decVd === "thorough") {
|
|
1326
|
+
this.overheadGovernor.overrideConfig({
|
|
1327
|
+
autoTsc: "BLOCKING",
|
|
1328
|
+
deepVerify: "BLOCKING",
|
|
1329
|
+
quickVerify: "BLOCKING",
|
|
1330
|
+
qaPipeline: "BLOCKING",
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
else if (decMode === "HYBRID") {
|
|
1334
|
+
this.overheadGovernor.overrideConfig({
|
|
1335
|
+
autoTsc: "SHADOW",
|
|
1336
|
+
quickVerify: "SHADOW",
|
|
1337
|
+
qaPipeline: "SHADOW",
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
// ── Initialize safety floor modules (MutationPolicy + PatchScopeController) ──
|
|
1341
|
+
if (this.config.loop.projectPath) {
|
|
1342
|
+
this.mutationPolicy = new WorkspaceMutationPolicy(this.config.loop.projectPath);
|
|
1343
|
+
const lifecycle = detectRepoLifecycle(this.config.loop.projectPath);
|
|
1344
|
+
this.patchScopeController = new PatchScopeController(this.decision.core.reasoning.complexity, lifecycle);
|
|
1345
|
+
}
|
|
1346
|
+
// ── Decision-based memory load override (Phase I+ SSOT) ──
|
|
1347
|
+
// If Decision says memory is not needed, clear already-loaded memory context
|
|
1348
|
+
// to reduce prompt size for trivial tasks.
|
|
1349
|
+
const shouldLoadMemory = this.decision.core.memoryLoad.shouldLoad;
|
|
1350
|
+
if (!shouldLoadMemory) {
|
|
1351
|
+
this.pendingRunContext.memoryContext = undefined;
|
|
1352
|
+
this.pendingRunContext.taskMemory = undefined;
|
|
1353
|
+
this.pendingRunContext.ragContext = undefined;
|
|
1354
|
+
}
|
|
1355
|
+
// ── Decision-based skill/specialist/background overrides (Phase I SSOT) ──
|
|
1356
|
+
const sa = this.decision.core.skillActivation;
|
|
1357
|
+
// Specialist routing from Decision
|
|
1358
|
+
if (sa.enableSpecialist && sa.specialistDomain) {
|
|
1359
|
+
const specialistMatch = this.specialistRegistry.findSpecialist(sa.specialistDomain);
|
|
1360
|
+
if (specialistMatch && specialistMatch.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD) {
|
|
1361
|
+
this.contextManager.addMessage({
|
|
1362
|
+
role: "system",
|
|
1363
|
+
content: `[Specialist: ${specialistMatch.specialist.name}] ${specialistMatch.specialist.systemPrompt.slice(0, 500)}`,
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1340
1366
|
}
|
|
1367
|
+
// Tool planning from Decision
|
|
1368
|
+
if (sa.enableToolPlanning) {
|
|
1369
|
+
const classification = this.taskClassifier.classify(userMessage);
|
|
1370
|
+
if (classification.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD) {
|
|
1371
|
+
const planContext = { userMessage };
|
|
1372
|
+
this.currentToolPlan = this.toolPlanner.planForTask(classification.type, planContext);
|
|
1373
|
+
this.executedToolNames = [];
|
|
1374
|
+
if (this.currentToolPlan.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD) {
|
|
1375
|
+
const planHint = this.toolPlanner.formatPlanHint(this.currentToolPlan);
|
|
1376
|
+
this.contextManager.addMessage({
|
|
1377
|
+
role: "system",
|
|
1378
|
+
content: planHint,
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
catch {
|
|
1385
|
+
this.decision = DEFAULT_DECISION;
|
|
1341
1386
|
}
|
|
1342
|
-
// 복잡도 감지 → 필요 시 자동 플래닝
|
|
1343
1387
|
await this.maybeCreatePlan(userMessage);
|
|
1344
|
-
// ContinuousReflection 시작 (1분 간격 체크포인트/자기검증/컨텍스트모니터)
|
|
1345
1388
|
if (this.continuousReflection) {
|
|
1346
1389
|
this.continuousReflection.start();
|
|
1347
1390
|
}
|
|
@@ -1350,20 +1393,19 @@ export class AgentLoop extends EventEmitter {
|
|
|
1350
1393
|
result = await this.executeLoop();
|
|
1351
1394
|
}
|
|
1352
1395
|
finally {
|
|
1353
|
-
// ContinuousReflection 정지 (루프 종료 시 반드시 정리)
|
|
1354
1396
|
if (this.continuousReflection) {
|
|
1355
1397
|
this.continuousReflection.stop();
|
|
1356
1398
|
}
|
|
1357
1399
|
}
|
|
1400
|
+
this.prevDecision = this.decision;
|
|
1401
|
+
this.toolUsageCounter = { reads: 0, edits: 0, shells: 0, tests: 0, searches: 0, webLookups: 0, sameFileEdits: new Map() };
|
|
1358
1402
|
if (this.sessionPersistence && this.sessionId) {
|
|
1359
1403
|
const finalStatus = result.reason === "ERROR"
|
|
1360
1404
|
? "crashed"
|
|
1361
1405
|
: "completed";
|
|
1362
1406
|
await this.sessionPersistence.updateStatus(this.sessionId, finalStatus);
|
|
1363
1407
|
}
|
|
1364
|
-
// 실행 완료 후 메모리 자동 업데이트
|
|
1365
1408
|
await this.updateMemoryAfterRun(userMessage, result, Date.now() - runStartTime);
|
|
1366
|
-
// Phase 4: FailureSignatureMemory promote + PlaybookLibrary outcome recording
|
|
1367
1409
|
if (result.reason === "GOAL_ACHIEVED") {
|
|
1368
1410
|
try {
|
|
1369
1411
|
if (this.failureSigMemory && this.repeatedErrorSignature) {
|
|
@@ -1378,7 +1420,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
1378
1420
|
}
|
|
1379
1421
|
catch { /* non-fatal */ }
|
|
1380
1422
|
}
|
|
1381
|
-
// Phase 4 remaining: SelfImprovementLoop + MetaLearningCollector + TrustEconomics
|
|
1382
1423
|
try {
|
|
1383
1424
|
const taskType = this.playbookLibrary?.query(userMessage)?.taskType ?? "unknown";
|
|
1384
1425
|
const championPlaybook = selectMarketStrategy(this.strategyMarket, taskType);
|
|
@@ -1419,7 +1460,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
1419
1460
|
this.trustEconomics.record(ac, tr.success);
|
|
1420
1461
|
}
|
|
1421
1462
|
}
|
|
1422
|
-
// Phase 5: record strategy outcome
|
|
1423
1463
|
if (this.strategyLearner) {
|
|
1424
1464
|
const activePlaybookId = this._activePlaybookId ?? "default";
|
|
1425
1465
|
if (runSuccess) {
|
|
@@ -1429,7 +1469,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
1429
1469
|
this.strategyLearner.recordFailure(activePlaybookId, taskType, this.iterationCount, this.tokenUsage.total);
|
|
1430
1470
|
}
|
|
1431
1471
|
}
|
|
1432
|
-
// Phase 5 extended: periodic pattern extraction (every 10 runs)
|
|
1433
1472
|
this.sessionRunCount += 1;
|
|
1434
1473
|
if (this.sessionRunCount % 10 === 0) {
|
|
1435
1474
|
if (this.tracePatternExtractor) {
|
|
@@ -1439,7 +1478,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
1439
1478
|
this.metaLearningEngine.analyze().catch(() => { });
|
|
1440
1479
|
}
|
|
1441
1480
|
}
|
|
1442
|
-
// Phase 6: record outcomes in capability self model and strategy market
|
|
1443
1481
|
try {
|
|
1444
1482
|
const env = this._detectedEnvironment ?? "general";
|
|
1445
1483
|
this.capabilitySelfModel?.recordOutcome(env, taskType, runSuccess);
|
|
@@ -1453,13 +1491,11 @@ export class AgentLoop extends EventEmitter {
|
|
|
1453
1491
|
catch { /* non-fatal */ }
|
|
1454
1492
|
}
|
|
1455
1493
|
catch { /* non-fatal */ }
|
|
1456
|
-
// SkillLearner: 성공적 에러 해결 시 새로운 스킬 학습
|
|
1457
1494
|
if (this.skillLearner && result.reason === "GOAL_ACHIEVED") {
|
|
1458
1495
|
try {
|
|
1459
1496
|
let newSkillId = null;
|
|
1460
1497
|
const errorToolResults = this.allToolResults.filter((r) => !r.success);
|
|
1461
1498
|
if (errorToolResults.length > 0 && this.changedFiles.length > 0) {
|
|
1462
|
-
// 에러가 있었지만 결국 성공 → 학습 가능한 패턴
|
|
1463
1499
|
const runAnalysis = this.memoryUpdater.analyzeRun({
|
|
1464
1500
|
goal: userMessage,
|
|
1465
1501
|
termination: { reason: result.reason, summary: result.summary },
|
|
@@ -1487,27 +1523,17 @@ export class AgentLoop extends EventEmitter {
|
|
|
1487
1523
|
}
|
|
1488
1524
|
}
|
|
1489
1525
|
}
|
|
1490
|
-
catch {
|
|
1491
|
-
// 학습 실패는 치명적이지 않음
|
|
1492
|
-
}
|
|
1526
|
+
catch { /* non-fatal */ }
|
|
1493
1527
|
}
|
|
1494
|
-
// Tool plan compliance check (non-blocking, for metrics)
|
|
1495
1528
|
if (this.currentToolPlan && this.executedToolNames.length > 0) {
|
|
1496
1529
|
try {
|
|
1497
1530
|
this.toolPlanner.validateExecution(this.currentToolPlan, this.executedToolNames);
|
|
1498
1531
|
}
|
|
1499
|
-
catch {
|
|
1500
|
-
// compliance check 실패는 치명적이지 않음
|
|
1501
|
-
}
|
|
1532
|
+
catch { /* non-fatal */ }
|
|
1502
1533
|
}
|
|
1503
|
-
// RepoKnowledgeGraph: 변경 파일 그래프 업데이트
|
|
1504
1534
|
if (this.repoGraph && this.changedFiles.length > 0) {
|
|
1505
|
-
this.repoGraph.updateFiles(this.changedFiles).catch(() => {
|
|
1506
|
-
// 그래프 업데이트 실패는 치명적이지 않음
|
|
1507
|
-
});
|
|
1535
|
+
this.repoGraph.updateFiles(this.changedFiles).catch(() => { });
|
|
1508
1536
|
}
|
|
1509
|
-
// BackgroundAgentManager: 정리 (stopAll은 abort 시에만)
|
|
1510
|
-
// Background agents는 세션 간 지속되므로 여기서 stop하지 않음
|
|
1511
1537
|
return result;
|
|
1512
1538
|
}
|
|
1513
1539
|
catch (err) {
|
|
@@ -1532,10 +1558,10 @@ export class AgentLoop extends EventEmitter {
|
|
|
1532
1558
|
* - 성공/실패 패턴 학습
|
|
1533
1559
|
*/
|
|
1534
1560
|
async updateMemoryAfterRun(userGoal, result, runDurationMs = 0) {
|
|
1535
|
-
|
|
1561
|
+
// Gate by both memoryLoad (loading) and memoryIntent (saving)
|
|
1562
|
+
if (!this.decision.core.memoryIntent.shouldSave || !this.memoryManager)
|
|
1536
1563
|
return;
|
|
1537
1564
|
try {
|
|
1538
|
-
// MemoryUpdater로 풍부한 학습 추출
|
|
1539
1565
|
const analysis = this.memoryUpdater.analyzeRun({
|
|
1540
1566
|
goal: userGoal,
|
|
1541
1567
|
termination: {
|
|
@@ -1558,16 +1584,13 @@ export class AgentLoop extends EventEmitter {
|
|
|
1558
1584
|
durationMs: runDurationMs,
|
|
1559
1585
|
iterations: this.iterationCount,
|
|
1560
1586
|
});
|
|
1561
|
-
// 추출된 학습을 MemoryManager에 저장
|
|
1562
1587
|
const learnings = this.memoryUpdater.extractLearnings(analysis, userGoal);
|
|
1563
1588
|
for (const learning of learnings) {
|
|
1564
1589
|
this.memoryManager.addLearning(learning.category, learning.content);
|
|
1565
1590
|
}
|
|
1566
|
-
// 감지된 컨벤션 저장 (기존에 추출만 되고 저장 안 되던 버그 수정)
|
|
1567
1591
|
for (const convention of analysis.conventions) {
|
|
1568
1592
|
this.memoryManager.addConvention(convention);
|
|
1569
1593
|
}
|
|
1570
|
-
// 감지된 패턴 저장
|
|
1571
1594
|
for (const pattern of analysis.toolPatterns) {
|
|
1572
1595
|
if (pattern.successRate > 0.7 && pattern.count >= 3) {
|
|
1573
1596
|
this.memoryManager.addPattern({
|
|
@@ -1578,25 +1601,18 @@ export class AgentLoop extends EventEmitter {
|
|
|
1578
1601
|
});
|
|
1579
1602
|
}
|
|
1580
1603
|
}
|
|
1581
|
-
// 에러로 종료된 경우 실패 기록도 추가
|
|
1582
1604
|
if (result.reason === "ERROR") {
|
|
1583
1605
|
this.memoryManager.addFailedApproach(`Task: ${userGoal.slice(0, 80)}`, result.error ?? "Unknown error");
|
|
1584
1606
|
}
|
|
1585
|
-
// 오래된 항목 정리 (매 5회 실행마다)
|
|
1586
1607
|
if (this.iterationCount % 5 === 0) {
|
|
1587
1608
|
this.memoryManager.prune();
|
|
1588
1609
|
}
|
|
1589
|
-
// 메모리 저장
|
|
1590
1610
|
await this.memoryManager.save();
|
|
1591
|
-
// PersonaManager — 유저 프로필 저장 (학습된 커뮤니케이션 스타일 유지)
|
|
1592
1611
|
if (this.personaManager) {
|
|
1593
1612
|
await this.personaManager.saveProfile().catch(() => { });
|
|
1594
1613
|
}
|
|
1595
1614
|
}
|
|
1596
|
-
catch {
|
|
1597
|
-
// 메모리 저장 실패는 치명적이지 않음
|
|
1598
|
-
}
|
|
1599
|
-
// Reflexion: 실행 결과 반영 + 전략 추출
|
|
1615
|
+
catch { /* non-fatal */ }
|
|
1600
1616
|
if (this.reflexionEngine) {
|
|
1601
1617
|
try {
|
|
1602
1618
|
const entry = this.reflexionEngine.reflect({
|
|
@@ -1610,7 +1626,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
1610
1626
|
changedFiles: this.changedFiles,
|
|
1611
1627
|
});
|
|
1612
1628
|
await this.reflexionEngine.store.saveReflection(entry);
|
|
1613
|
-
// 성공 시 전략 추출
|
|
1614
1629
|
if (entry.outcome === "success") {
|
|
1615
1630
|
const strategy = this.reflexionEngine.extractStrategy(entry, userGoal);
|
|
1616
1631
|
if (strategy) {
|
|
@@ -1618,9 +1633,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
1618
1633
|
}
|
|
1619
1634
|
}
|
|
1620
1635
|
}
|
|
1621
|
-
catch {
|
|
1622
|
-
// reflexion 저장 실패는 치명적이지 않음
|
|
1623
|
-
}
|
|
1636
|
+
catch { /* non-fatal */ }
|
|
1624
1637
|
}
|
|
1625
1638
|
}
|
|
1626
1639
|
/**
|
|
@@ -1747,11 +1760,28 @@ export class AgentLoop extends EventEmitter {
|
|
|
1747
1760
|
lines.push(`changed: ${files}`);
|
|
1748
1761
|
}
|
|
1749
1762
|
lines.push(`token_budget: ${remaining}% remaining`);
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1763
|
+
this.ephemeralHints.push(lines.join(" | "));
|
|
1764
|
+
this.lastAgentStateInjection = iteration;
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Phase B: flush collected ephemeral hints into context as system messages.
|
|
1768
|
+
* Applies rate-limiting: max 7 hints, max 3000 tokens total (matching PromptRuntime compileEphemeral).
|
|
1769
|
+
* Called at end of each iteration (before next LLM call) and before `continue` statements.
|
|
1770
|
+
*/
|
|
1771
|
+
flushEphemeralHints() {
|
|
1772
|
+
if (this.ephemeralHints.length === 0)
|
|
1773
|
+
return;
|
|
1774
|
+
const MAX_HINTS = 7;
|
|
1775
|
+
const MAX_TOKENS = 3000;
|
|
1776
|
+
let tokenCount = 0;
|
|
1777
|
+
for (const hint of this.ephemeralHints.slice(0, MAX_HINTS)) {
|
|
1778
|
+
const tokens = Math.ceil(hint.length / 3.5);
|
|
1779
|
+
if (tokenCount + tokens > MAX_TOKENS)
|
|
1780
|
+
break;
|
|
1781
|
+
tokenCount += tokens;
|
|
1782
|
+
this.contextManager.addMessage({ role: "system", content: hint });
|
|
1754
1783
|
}
|
|
1784
|
+
this.ephemeralHints = [];
|
|
1755
1785
|
}
|
|
1756
1786
|
/**
|
|
1757
1787
|
* LLM 응답 텍스트에서 "Updated hypothesis:" 마커를 파싱해 hypothesis를 갱신한다.
|
|
@@ -1816,24 +1846,11 @@ export class AgentLoop extends EventEmitter {
|
|
|
1816
1846
|
* - "moderate" 이상 → HierarchicalPlanner로 L1+L2 계획 수립
|
|
1817
1847
|
*/
|
|
1818
1848
|
async maybeCreatePlan(userMessage) {
|
|
1819
|
-
if (!this.planner
|
|
1849
|
+
if (!this.planner)
|
|
1820
1850
|
return;
|
|
1821
|
-
|
|
1822
|
-
this.currentComplexity = complexity;
|
|
1823
|
-
// 임계값 미만이면 플래닝 스킵
|
|
1824
|
-
// Bug 4 fix: extend thresholdOrder to include "massive" (4), so that when planningThreshold
|
|
1825
|
-
// is "complex", both "complex" (3) and "massive" (4) trigger planning.
|
|
1826
|
-
// Previously "massive" had no entry and fell through to undefined → NaN comparisons.
|
|
1827
|
-
const thresholdOrder = { simple: 1, moderate: 2, complex: 3, massive: 4 };
|
|
1828
|
-
const complexityOrder = {
|
|
1829
|
-
trivial: 0, simple: 1, moderate: 2, complex: 3, massive: 4,
|
|
1830
|
-
};
|
|
1831
|
-
// Use the threshold for the configured level; "complex" threshold activates for complexity >= "complex"
|
|
1832
|
-
const effectiveThreshold = thresholdOrder[this.planningThreshold] ?? 2;
|
|
1833
|
-
if ((complexityOrder[complexity] ?? 0) < effectiveThreshold) {
|
|
1851
|
+
if (!this.decision.core.planRequired)
|
|
1834
1852
|
return;
|
|
1835
|
-
}
|
|
1836
|
-
this.emitSubagent("planner", "start", `task complexity ${complexity}. creating execution plan`);
|
|
1853
|
+
this.emitSubagent("planner", "start", `task complexity ${this.currentComplexity}. creating execution plan`);
|
|
1837
1854
|
try {
|
|
1838
1855
|
const plan = await this.planner.createHierarchicalPlan(userMessage, this.llmClient);
|
|
1839
1856
|
this.activePlan = plan;
|
|
@@ -1869,13 +1886,8 @@ export class AgentLoop extends EventEmitter {
|
|
|
1869
1886
|
});
|
|
1870
1887
|
}
|
|
1871
1888
|
}
|
|
1872
|
-
/**
|
|
1873
|
-
* 사용자 메시지에서 태스크 복잡도를 휴리스틱으로 추정.
|
|
1874
|
-
* LLM 호출 없이 빠르게 결정 (토큰 절약).
|
|
1875
|
-
*/
|
|
1876
1889
|
/**
|
|
1877
1890
|
* Detect the best test/verify command for the current project.
|
|
1878
|
-
* Bug 3 fix: replaces the hardcoded "pnpm build" default.
|
|
1879
1891
|
*/
|
|
1880
1892
|
detectTestCommand() {
|
|
1881
1893
|
const projectPath = this.config.loop.projectPath;
|
|
@@ -1907,6 +1919,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
1907
1919
|
}
|
|
1908
1920
|
return "pnpm build";
|
|
1909
1921
|
}
|
|
1922
|
+
/** @deprecated Use AgentDecisionContext.core.reasoning.complexity instead */
|
|
1910
1923
|
_detectComplexityHeuristic(message) {
|
|
1911
1924
|
const lower = message.toLowerCase();
|
|
1912
1925
|
const len = message.length;
|
|
@@ -2011,10 +2024,9 @@ export class AgentLoop extends EventEmitter {
|
|
|
2011
2024
|
return "massive";
|
|
2012
2025
|
}
|
|
2013
2026
|
/**
|
|
2014
|
-
*
|
|
2015
|
-
* LLM
|
|
2016
|
-
*
|
|
2017
|
-
* (ambitious short requests that keywords miss across any language).
|
|
2027
|
+
* @deprecated Use AgentDecisionContext.core.reasoning.complexity instead.
|
|
2028
|
+
* Hybrid complexity detection: keyword heuristic + LLM fallback for borderline cases.
|
|
2029
|
+
* Only called when Decision Engine is absent.
|
|
2018
2030
|
*/
|
|
2019
2031
|
async detectComplexity(message) {
|
|
2020
2032
|
const heuristic = this._detectComplexityHeuristic(message);
|
|
@@ -2117,11 +2129,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
2117
2129
|
this.activePlan.tactical.push(modTask);
|
|
2118
2130
|
}
|
|
2119
2131
|
}
|
|
2120
|
-
|
|
2121
|
-
this.contextManager.addMessage({
|
|
2122
|
-
role: "system",
|
|
2123
|
-
content: `[Re-plan] Strategy: ${result.strategy}. Reason: ${result.reason}.\nModified tasks: ${result.modifiedTasks.map((t) => t.description).join(", ")}`,
|
|
2124
|
-
});
|
|
2132
|
+
this.ephemeralHints.push(`[Re-plan] Strategy: ${result.strategy}. Reason: ${result.reason}.\nModified tasks: ${result.modifiedTasks.map((t) => t.description).join(", ")}`);
|
|
2125
2133
|
}
|
|
2126
2134
|
// Estimate replan token usage
|
|
2127
2135
|
this.tokenBudgetManager.recordUsage("planner", 500, 500);
|
|
@@ -2178,22 +2186,18 @@ export class AgentLoop extends EventEmitter {
|
|
|
2178
2186
|
this.iterationCount = iteration;
|
|
2179
2187
|
dlog("AGENT-LOOP", `── iteration ${iteration} start ──`, { tokenUsageTotal: this.tokenUsage.total });
|
|
2180
2188
|
const iterationStart = Date.now();
|
|
2181
|
-
// Phase 6: Budget governor halt check
|
|
2182
2189
|
dlog("AGENT-LOOP", `BUDGET_EXHAUSTED check`, { used: this.tokenUsage.total, budget: this.config.loop.totalTokenBudget });
|
|
2183
2190
|
if (checkBudgetShouldHalt(this.budgetGovernorV2, this.sessionId ?? "default")) {
|
|
2184
2191
|
return { reason: "BUDGET_EXHAUSTED", tokensUsed: this.tokenUsage.total };
|
|
2185
2192
|
}
|
|
2186
|
-
// Reset per-iteration governor state
|
|
2187
2193
|
this.verifyRanThisIteration = false;
|
|
2188
2194
|
this.summarizeRanThisIteration = false;
|
|
2189
2195
|
this.llmFixerRunCount = 0;
|
|
2190
2196
|
this.iterationSystemMsgCount = 0;
|
|
2191
|
-
|
|
2197
|
+
this.ephemeralHints = [];
|
|
2192
2198
|
this.pruneMessagesIfNeeded();
|
|
2193
|
-
// Cap allToolResults (prevents unbounded memory growth)
|
|
2194
2199
|
this.allToolResults = cap(this.allToolResults, BOUNDS.allToolResults);
|
|
2195
2200
|
this.allToolResultsSinceLastReplan = cap(this.allToolResultsSinceLastReplan, BOUNDS.toolResultsSinceReplan);
|
|
2196
|
-
// Proactive replanning check (every 10 iterations when plan is active)
|
|
2197
2201
|
if (this.replanningEngine &&
|
|
2198
2202
|
this.activePlan &&
|
|
2199
2203
|
this.activeMilestones.length > 0 &&
|
|
@@ -2217,38 +2221,21 @@ export class AgentLoop extends EventEmitter {
|
|
|
2217
2221
|
this.activePlan.tactical[idx] = modified;
|
|
2218
2222
|
}
|
|
2219
2223
|
}
|
|
2220
|
-
// Reset tool results accumulator after replan
|
|
2221
2224
|
this.allToolResultsSinceLastReplan = [];
|
|
2222
|
-
|
|
2223
|
-
this.contextManager.addMessage({
|
|
2224
|
-
role: "system",
|
|
2225
|
-
content: `[Proactive Replan] ${replanResult.message}\nScope: ${replanResult.decision.scope}, Risk: ${replanResult.decision.urgency}`,
|
|
2226
|
-
});
|
|
2225
|
+
this.ephemeralHints.push(`[Proactive Replan] ${replanResult.message}\nScope: ${replanResult.decision.scope}, Risk: ${replanResult.decision.urgency}`);
|
|
2227
2226
|
}
|
|
2228
2227
|
}
|
|
2229
|
-
catch {
|
|
2230
|
-
// Non-blocking — proactive replanning failures should not crash the agent
|
|
2231
|
-
}
|
|
2228
|
+
catch { /* non-fatal */ }
|
|
2232
2229
|
}
|
|
2233
|
-
// Plan progress injection — every 3 iterations or when task advances
|
|
2234
2230
|
if (this.activePlan) {
|
|
2235
2231
|
this.injectPlanProgress(iteration);
|
|
2236
2232
|
}
|
|
2237
|
-
// NOTE: ExecutionPolicyEngine cost.maxTokensPerIteration check was removed.
|
|
2238
|
-
// That check compared cumulative token totals against a per-iteration limit (50k default),
|
|
2239
|
-
// causing premature BUDGET_EXHAUSTED at ~50k total tokens regardless of configured totalTokenBudget.
|
|
2240
|
-
// Budget enforcement is correctly handled by BudgetGovernorV2 (taskBudget: 200k) above
|
|
2241
|
-
// and the totalTokenBudget check at the end of each iteration.
|
|
2242
|
-
// Soft context rollover:
|
|
2243
|
-
// checkpoint first, then let ContextManager compact instead of aborting/throwing.
|
|
2244
2233
|
const contextUsageRatio = this.contextManager.getUsageRatio();
|
|
2245
|
-
// Task 1: ContextBudgetManager LLM summarization — Governor gated (default SHADOW)
|
|
2246
2234
|
const summarizeMode = this.overheadGovernor.shouldRunSummarize(this.buildTriggerContext());
|
|
2247
2235
|
if (summarizeMode === "BLOCKING")
|
|
2248
2236
|
this.summarizeRanThisIteration = true;
|
|
2249
2237
|
if (contextUsageRatio >= 0.75 && this.contextBudgetManager && !this._contextSummarizationDone && summarizeMode === "BLOCKING") {
|
|
2250
|
-
this._contextSummarizationDone = true;
|
|
2251
|
-
// Non-blocking: fire-and-forget so the main iteration is not stalled
|
|
2238
|
+
this._contextSummarizationDone = true;
|
|
2252
2239
|
this.contextBudgetManager.importMessages(this.contextManager.getMessages());
|
|
2253
2240
|
if (this.contextBudgetManager.needsSummarization()) {
|
|
2254
2241
|
const budgetMgr = this.contextBudgetManager;
|
|
@@ -2263,13 +2250,9 @@ export class AgentLoop extends EventEmitter {
|
|
|
2263
2250
|
content: `Context at ${Math.round(ratio * 100)}%: summarized ${summary.originalIds.length} old messages (${summary.originalTokens} → ${summary.summarizedTokens} tokens, ${Math.round(summary.compressionRatio * 100)}% ratio).`,
|
|
2264
2251
|
});
|
|
2265
2252
|
}
|
|
2266
|
-
}).catch(() => {
|
|
2267
|
-
// summarization failure is non-fatal
|
|
2268
|
-
});
|
|
2253
|
+
}).catch(() => { });
|
|
2269
2254
|
}
|
|
2270
2255
|
}
|
|
2271
|
-
// ContextCompressor block removed: prepareForLLM() already handles compression internally.
|
|
2272
|
-
// Double compression (ContextCompressor + prepareForLLM) caused redundant message eviction.
|
|
2273
2256
|
if (contextUsageRatio >= 0.85) {
|
|
2274
2257
|
if (!this.checkpointSaved) {
|
|
2275
2258
|
await this.saveAutoCheckpoint(iteration);
|
|
@@ -2281,9 +2264,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
2281
2264
|
`Compressing conversation state and continuing.`,
|
|
2282
2265
|
});
|
|
2283
2266
|
}
|
|
2284
|
-
// Check file-pattern skill triggers based on changed files
|
|
2285
|
-
// Guard: skip skill injection if over 80% token budget to preserve remaining budget
|
|
2286
|
-
// Guard: max 3 active skills globally (Context Budget Rules)
|
|
2287
2267
|
const fileTriggerBudgetRatio = this.config.loop.totalTokenBudget > 0
|
|
2288
2268
|
? this.tokenUsage.total / this.config.loop.totalTokenBudget
|
|
2289
2269
|
: 0;
|
|
@@ -2294,19 +2274,14 @@ export class AgentLoop extends EventEmitter {
|
|
|
2294
2274
|
const fileSkills = this.pluginRegistry.findMatchingSkills({
|
|
2295
2275
|
filePath: lastFile,
|
|
2296
2276
|
});
|
|
2297
|
-
// Max 2 skills per iteration, capped by global 3-skill limit
|
|
2298
2277
|
const slotsRemaining = AgentLoop.MAX_ACTIVE_SKILLS - this.activeSkillIds.length;
|
|
2299
2278
|
for (const skill of fileSkills.slice(0, Math.min(2, slotsRemaining))) {
|
|
2300
2279
|
if (this.activeSkillIds.includes(skill.id))
|
|
2301
2280
|
continue; // no duplicate
|
|
2302
2281
|
const parsed = this.skillLoader.loadTemplate(skill);
|
|
2303
2282
|
if (parsed) {
|
|
2304
|
-
this.
|
|
2305
|
-
role: "system",
|
|
2306
|
-
content: `[File Skill: ${skill.name}] ${parsed.domain ? `[${parsed.domain}] ` : ""}${skill.description}`,
|
|
2307
|
-
});
|
|
2283
|
+
this.ephemeralHints.push(`[File Skill: ${skill.name}] ${parsed.domain ? `[${parsed.domain}] ` : ""}${skill.description}`);
|
|
2308
2284
|
this.activeSkillIds.push(skill.id);
|
|
2309
|
-
this.iterationSystemMsgCount++;
|
|
2310
2285
|
}
|
|
2311
2286
|
}
|
|
2312
2287
|
}
|
|
@@ -2468,10 +2443,9 @@ export class AgentLoop extends EventEmitter {
|
|
|
2468
2443
|
role: "assistant",
|
|
2469
2444
|
content: content || "",
|
|
2470
2445
|
});
|
|
2471
|
-
this.
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
});
|
|
2446
|
+
this.ephemeralHints.push(`[Self-Reflection L2] Verification failed (score: ${deepResult.overallScore}, confidence: ${deepResult.confidence.toFixed(2)}). ${deepResult.selfCritique}${issuesList ? ` Issues: ${issuesList}` : ""}. Please address these before completing.`);
|
|
2447
|
+
// Flush ephemeral hints before continue (next iteration resets them)
|
|
2448
|
+
this.flushEphemeralHints();
|
|
2475
2449
|
this.emitSubagent("verifier", "done", `deep verification failed, score ${deepResult.overallScore}. continuing to address issues`);
|
|
2476
2450
|
continue; // Don't return GOAL_ACHIEVED, continue the loop
|
|
2477
2451
|
}
|
|
@@ -2500,10 +2474,9 @@ export class AgentLoop extends EventEmitter {
|
|
|
2500
2474
|
role: "assistant",
|
|
2501
2475
|
content: content || "",
|
|
2502
2476
|
});
|
|
2503
|
-
this.
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
});
|
|
2477
|
+
this.ephemeralHints.push(`[Debate] Multi-agent debate did not pass (score: ${debateResult.finalScore}). ${debateResult.summary}. Please address the identified issues.`);
|
|
2478
|
+
// Flush ephemeral hints before continue
|
|
2479
|
+
this.flushEphemeralHints();
|
|
2507
2480
|
this.emitEvent({
|
|
2508
2481
|
kind: "agent:thinking",
|
|
2509
2482
|
content: `Debate failed (score: ${debateResult.finalScore}). Continuing to address issues...`,
|
|
@@ -2536,7 +2509,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
2536
2509
|
content: finalSummary,
|
|
2537
2510
|
});
|
|
2538
2511
|
}
|
|
2539
|
-
// Task 2: QAPipeline "thorough" mode at final task completion (LLM review included)
|
|
2540
2512
|
if (this.changedFiles.length > 0 && this.config.loop.projectPath) {
|
|
2541
2513
|
try {
|
|
2542
2514
|
const thoroughQA = new QAPipeline({
|
|
@@ -2600,18 +2572,23 @@ export class AgentLoop extends EventEmitter {
|
|
|
2600
2572
|
if (taskCompleteCall) {
|
|
2601
2573
|
const callArgs = this.parseToolArgs(taskCompleteCall.arguments);
|
|
2602
2574
|
const taskCompleteSummary = String(callArgs["summary"] ?? response.content ?? "Task completed.");
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2575
|
+
if (this.decision.core.vetoFlags.verifyRequired && !this.verifyRanThisIteration) {
|
|
2576
|
+
this.ephemeralHints.push("[VERIFY REQUIRED] You must run build/test verification before completing. Run tsc, tests, or verification commands now.");
|
|
2577
|
+
// Don't return completion — continue loop so LLM runs verification first
|
|
2578
|
+
this.contextManager.addMessage({
|
|
2579
|
+
role: "assistant",
|
|
2580
|
+
content: response.content,
|
|
2581
|
+
tool_calls: response.toolCalls.filter((tc) => tc.name !== "task_complete"),
|
|
2582
|
+
});
|
|
2583
|
+
continue;
|
|
2584
|
+
}
|
|
2585
|
+
// Filter task_complete from tool_calls — internal protocol signal without matching tool result
|
|
2608
2586
|
const nonProtocolCalls = response.toolCalls.filter((tc) => tc.name !== "task_complete");
|
|
2609
2587
|
this.contextManager.addMessage({
|
|
2610
2588
|
role: "assistant",
|
|
2611
2589
|
content: response.content,
|
|
2612
2590
|
tool_calls: nonProtocolCalls.length > 0 ? nonProtocolCalls : undefined,
|
|
2613
2591
|
});
|
|
2614
|
-
// FIX: Emit tool_result so TUI marks task_complete as "success" (stops BlinkingDot + LiveElapsed)
|
|
2615
2592
|
this.emitEvent({
|
|
2616
2593
|
kind: "agent:tool_result",
|
|
2617
2594
|
tool: "task_complete",
|
|
@@ -2649,18 +2626,12 @@ export class AgentLoop extends EventEmitter {
|
|
|
2649
2626
|
}
|
|
2650
2627
|
// 4. 도구 실행
|
|
2651
2628
|
const { results: toolResults, deferredFixPrompts } = await this.executeTools(response.toolCalls);
|
|
2652
|
-
// Reflexion: 도구 결과 수집
|
|
2653
2629
|
this.allToolResults.push(...toolResults);
|
|
2654
2630
|
const failedResults = toolResults.filter(r => !r.success);
|
|
2655
|
-
// Tool failures are already in tool result messages — LLM sees them naturally.
|
|
2656
|
-
// Removed forced retry logic that caused recovery loops.
|
|
2657
|
-
// Phase 6: Record tool outcomes in capability graph
|
|
2658
2631
|
for (const tr of toolResults) {
|
|
2659
2632
|
recordToolOutcomeInGraph(this.capabilityGraph, tr.name, tr.success);
|
|
2660
2633
|
}
|
|
2661
|
-
// Accumulate tool results for proactive replanning evaluation
|
|
2662
2634
|
this.allToolResultsSinceLastReplan.push(...toolResults);
|
|
2663
|
-
// Tool plan tracking: 실행된 도구 이름 기록
|
|
2664
2635
|
for (const result of toolResults) {
|
|
2665
2636
|
this.executedToolNames.push(result.name);
|
|
2666
2637
|
}
|
|
@@ -2718,7 +2689,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
2718
2689
|
content: fixPrompt,
|
|
2719
2690
|
});
|
|
2720
2691
|
}
|
|
2721
|
-
// Task 2: QAPipeline — run "quick" (structural only) after any WRITE tool call this iteration
|
|
2722
2692
|
const projectPath = this.config.loop.projectPath;
|
|
2723
2693
|
const qaMode = this.overheadGovernor.shouldRunQaPipeline(this.buildTriggerContext());
|
|
2724
2694
|
if (this.iterationWriteToolPaths.length > 0 && projectPath && qaMode !== "OFF") {
|
|
@@ -2749,25 +2719,19 @@ export class AgentLoop extends EventEmitter {
|
|
|
2749
2719
|
issues: qaIssues,
|
|
2750
2720
|
});
|
|
2751
2721
|
// Only inject into LLM context in BLOCKING mode (SHADOW = observe only)
|
|
2752
|
-
if (qaMode === "BLOCKING" && failedChecks.length > 0
|
|
2722
|
+
if (qaMode === "BLOCKING" && failedChecks.length > 0) {
|
|
2753
2723
|
const checkSummary = failedChecks
|
|
2754
2724
|
.slice(0, 5)
|
|
2755
2725
|
.map((c) => ` - [${c.severity}] ${c.name}: ${c.message}`)
|
|
2756
2726
|
.join("\n");
|
|
2757
|
-
this.
|
|
2758
|
-
role: "system",
|
|
2759
|
-
content: `[QA Quick Check] ${failedChecks.length} issue(s) detected in modified files:\n${checkSummary}`,
|
|
2760
|
-
});
|
|
2761
|
-
this.iterationSystemMsgCount++;
|
|
2727
|
+
this.ephemeralHints.push(`[QA Quick Check] ${failedChecks.length} issue(s) detected in modified files:\n${checkSummary}`);
|
|
2762
2728
|
}
|
|
2763
2729
|
}
|
|
2764
2730
|
catch {
|
|
2765
2731
|
// QAPipeline failure is non-fatal
|
|
2766
2732
|
}
|
|
2767
2733
|
}
|
|
2768
|
-
// Reset per-iteration write tool tracking
|
|
2769
2734
|
this.iterationWriteToolPaths = [];
|
|
2770
|
-
// Phase 4: StallDetector check (non-blocking observer)
|
|
2771
2735
|
if (this.stallDetector) {
|
|
2772
2736
|
try {
|
|
2773
2737
|
const stallResult = this.stallDetector.check(this.iterationCount, this.changedFiles, this.repeatedErrorSignature);
|
|
@@ -2784,8 +2748,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
2784
2748
|
}
|
|
2785
2749
|
catch { /* non-fatal */ }
|
|
2786
2750
|
}
|
|
2787
|
-
// Task 3: Auto-run tsc --noEmit after 2+ TS files modified in this iteration
|
|
2788
|
-
// Skip if tsc was already run in the previous iteration (cooldown)
|
|
2789
2751
|
const tscFilesThisIteration = [...this.iterationTsFilesModified];
|
|
2790
2752
|
this.iterationTsFilesModified = []; // reset for next iteration
|
|
2791
2753
|
const tscRanPrev = this.tscRanLastIteration;
|
|
@@ -2813,15 +2775,11 @@ export class AgentLoop extends EventEmitter {
|
|
|
2813
2775
|
content: `Auto-TSC: TypeScript errors found after editing ${tscFilesThisIteration.join(", ")}.`,
|
|
2814
2776
|
});
|
|
2815
2777
|
// Only inject into LLM context in BLOCKING mode
|
|
2816
|
-
if (tscMode === "BLOCKING"
|
|
2778
|
+
if (tscMode === "BLOCKING") {
|
|
2817
2779
|
const truncated = tscOutput.length > 2000
|
|
2818
2780
|
? tscOutput.slice(0, 2000) + "\n[...tsc output truncated]"
|
|
2819
2781
|
: tscOutput;
|
|
2820
|
-
this.
|
|
2821
|
-
role: "system",
|
|
2822
|
-
content: `[Auto-TSC] TypeScript errors detected after modifying ${tscFilesThisIteration.length} files:\n\`\`\`\n${truncated}\n\`\`\`\nPlease fix these type errors.`,
|
|
2823
|
-
});
|
|
2824
|
-
this.iterationSystemMsgCount++;
|
|
2782
|
+
this.ephemeralHints.push(`[Auto-TSC] TypeScript errors detected after modifying ${tscFilesThisIteration.length} files:\n\`\`\`\n${truncated}\n\`\`\`\nPlease fix these type errors.`);
|
|
2825
2783
|
}
|
|
2826
2784
|
}
|
|
2827
2785
|
else {
|
|
@@ -2879,12 +2837,8 @@ export class AgentLoop extends EventEmitter {
|
|
|
2879
2837
|
(iterReflection.reflection.whatFailed.length > 0
|
|
2880
2838
|
? iterReflection.reflection.whatFailed.slice(0, 2).join("; ")
|
|
2881
2839
|
: null);
|
|
2882
|
-
if (insight && insight.length > 10
|
|
2883
|
-
this.
|
|
2884
|
-
role: "system",
|
|
2885
|
-
content: `[Reflection] ${insight}`,
|
|
2886
|
-
});
|
|
2887
|
-
this.iterationSystemMsgCount++;
|
|
2840
|
+
if (insight && insight.length > 10) {
|
|
2841
|
+
this.ephemeralHints.push(`[Reflection] ${insight}`);
|
|
2888
2842
|
}
|
|
2889
2843
|
}
|
|
2890
2844
|
catch {
|
|
@@ -2910,10 +2864,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
2910
2864
|
.filter(([, dim]) => dim.issues.length > 0)
|
|
2911
2865
|
.flatMap(([, dim]) => dim.issues);
|
|
2912
2866
|
if (issues.length > 0) {
|
|
2913
|
-
this.
|
|
2914
|
-
role: "system",
|
|
2915
|
-
content: `[Self-Reflection L1] Issues detected: ${issues.join(", ")}. Confidence: ${quickResult.confidence}`,
|
|
2916
|
-
});
|
|
2867
|
+
this.ephemeralHints.push(`[Self-Reflection L1] Issues detected: ${issues.join(", ")}. Confidence: ${quickResult.confidence}`);
|
|
2917
2868
|
this.emitSubagent("verifier", "done", `quick verification flagged ${issues.length} issues, confidence ${quickResult.confidence.toFixed(2)}`);
|
|
2918
2869
|
}
|
|
2919
2870
|
else {
|
|
@@ -2960,7 +2911,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
2960
2911
|
source: "agent",
|
|
2961
2912
|
});
|
|
2962
2913
|
}
|
|
2963
|
-
else
|
|
2914
|
+
else {
|
|
2964
2915
|
// retry, approach_change, scope_reduce → 복구 프롬프트 주입
|
|
2965
2916
|
// Context Budget: consolidated error guidance (recovery + debug in one message)
|
|
2966
2917
|
const recoveryPrompt = this.failureRecovery.buildRecoveryPrompt(decision, {
|
|
@@ -2978,7 +2929,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
2978
2929
|
const rootCauseAnalysis = this.selfDebugLoop.analyzeError(errorSummary);
|
|
2979
2930
|
if (rootCauseAnalysis.confidence >= 0.5) {
|
|
2980
2931
|
const debugStrategy = this.selfDebugLoop.selectStrategy(iteration - 2, []);
|
|
2981
|
-
// Bug 3 fix: use dynamic test command detection instead of hardcoded "pnpm build"
|
|
2982
2932
|
const testCmd = this.config.loop.testCommand
|
|
2983
2933
|
?? this.detectTestCommand();
|
|
2984
2934
|
const debugPrompt = this.selfDebugLoop.buildFixPrompt(debugStrategy, {
|
|
@@ -2990,7 +2940,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
2990
2940
|
currentStrategy: debugStrategy,
|
|
2991
2941
|
});
|
|
2992
2942
|
debugSuffix = `\n\n[SelfDebug L${Math.min(iteration - 2, 5)}] Strategy: ${debugStrategy}\n${debugPrompt}`;
|
|
2993
|
-
// Bug 2 fix: wire a real llmFixer so selfDebugLoop.debug() can call LLM
|
|
2994
2943
|
if (debugStrategy !== "escalate") {
|
|
2995
2944
|
this.llmFixerRunCount++;
|
|
2996
2945
|
this.selfDebugLoop.debug({
|
|
@@ -3017,11 +2966,20 @@ export class AgentLoop extends EventEmitter {
|
|
|
3017
2966
|
}
|
|
3018
2967
|
}
|
|
3019
2968
|
}
|
|
3020
|
-
this.
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
2969
|
+
this.ephemeralHints.push(recoveryPrompt + debugSuffix);
|
|
2970
|
+
}
|
|
2971
|
+
// CausalChainResolver: root cause analysis for code-related errors
|
|
2972
|
+
if (this.failureSigMemory && projectPath) {
|
|
2973
|
+
try {
|
|
2974
|
+
const resolver = new CausalChainResolver(this.failureSigMemory);
|
|
2975
|
+
const causal = await resolver.resolve(errorSummary, this.changedFiles, projectPath);
|
|
2976
|
+
if (causal && causal.confidence > 0.5) {
|
|
2977
|
+
this.ephemeralHints.push(`[ROOT_CAUSE] ${causal.suspectedRootCause} (confidence: ${causal.confidence.toFixed(2)})` +
|
|
2978
|
+
(causal.affectedFiles.length > 0 ? `\nAffected: ${causal.affectedFiles.join(", ")}` : "") +
|
|
2979
|
+
(causal.recommendedStrategy ? `\nStrategy: ${causal.recommendedStrategy}` : ""));
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
catch { /* CausalChainResolver failure is non-fatal */ }
|
|
3025
2983
|
}
|
|
3026
2984
|
// HierarchicalPlanner 리플래닝도 시도
|
|
3027
2985
|
if (this.activePlan) {
|
|
@@ -3032,7 +2990,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
3032
2990
|
? this.tokenUsage.total / this.config.loop.totalTokenBudget
|
|
3033
2991
|
: 0;
|
|
3034
2992
|
// Context Budget: skip all optional injections at 85%+ budget
|
|
3035
|
-
if (errorTriggerBudgetRatio <= 0.85
|
|
2993
|
+
if (errorTriggerBudgetRatio <= 0.85) {
|
|
3036
2994
|
// SkillLearner: 학습된 스킬 중 현재 에러에 매칭되는 것 주입
|
|
3037
2995
|
if (this.skillLearner && this.activeSkillIds.length < AgentLoop.MAX_ACTIVE_SKILLS) {
|
|
3038
2996
|
try {
|
|
@@ -3043,22 +3001,17 @@ export class AgentLoop extends EventEmitter {
|
|
|
3043
3001
|
for (const skill of relevantSkills.slice(0, 1)) {
|
|
3044
3002
|
if (this.activeSkillIds.includes(skill.id))
|
|
3045
3003
|
continue;
|
|
3046
|
-
this.
|
|
3047
|
-
role: "system",
|
|
3048
|
-
content: `[Learned Skill: ${skill.id}] Diagnosis: ${skill.diagnosis}\nStrategy: ${skill.strategy}\nTools: ${skill.toolSequence.join(" → ")}`,
|
|
3049
|
-
});
|
|
3004
|
+
this.ephemeralHints.push(`[Learned Skill: ${skill.id}] Diagnosis: ${skill.diagnosis}\nStrategy: ${skill.strategy}\nTools: ${skill.toolSequence.join(" → ")}`);
|
|
3050
3005
|
this.activeSkillIds.push(skill.id);
|
|
3051
|
-
this.iterationSystemMsgCount++;
|
|
3052
3006
|
this.skillLearner.updateConfidence(skill.id, false);
|
|
3053
3007
|
}
|
|
3054
3008
|
}
|
|
3055
3009
|
catch {
|
|
3056
|
-
|
|
3010
|
+
/* non-fatal */
|
|
3057
3011
|
}
|
|
3058
3012
|
}
|
|
3059
3013
|
// Plugin trigger matching — match errors/context to plugin skills
|
|
3060
|
-
if (this.activeSkillIds.length < AgentLoop.MAX_ACTIVE_SKILLS
|
|
3061
|
-
this.iterationSystemMsgCount < 5) {
|
|
3014
|
+
if (this.activeSkillIds.length < AgentLoop.MAX_ACTIVE_SKILLS) {
|
|
3062
3015
|
const triggerMatches = this.pluginRegistry.matchTriggers({
|
|
3063
3016
|
errorMessage: errorSummary,
|
|
3064
3017
|
taskDescription: "",
|
|
@@ -3076,12 +3029,8 @@ export class AgentLoop extends EventEmitter {
|
|
|
3076
3029
|
if (resolved.length > MAX_SKILL_INJECT_CHARS) {
|
|
3077
3030
|
resolved = resolved.slice(0, MAX_SKILL_INJECT_CHARS) + "\n[...truncated for token budget]";
|
|
3078
3031
|
}
|
|
3079
|
-
this.
|
|
3080
|
-
role: "system",
|
|
3081
|
-
content: `[Plugin Skill: ${bestMatch.pluginId}/${bestMatch.skill.name}]\n${resolved}`,
|
|
3082
|
-
});
|
|
3032
|
+
this.ephemeralHints.push(`[Plugin Skill: ${bestMatch.pluginId}/${bestMatch.skill.name}]\n${resolved}`);
|
|
3083
3033
|
this.activeSkillIds.push(bestMatch.skill.id);
|
|
3084
|
-
this.iterationSystemMsgCount++;
|
|
3085
3034
|
this.emitEvent({
|
|
3086
3035
|
kind: "agent:thinking",
|
|
3087
3036
|
content: `Activated plugin skill: ${bestMatch.skill.name} from ${bestMatch.pluginId}`,
|
|
@@ -3116,9 +3065,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
3116
3065
|
createdAt: new Date(),
|
|
3117
3066
|
});
|
|
3118
3067
|
}
|
|
3119
|
-
catch {
|
|
3120
|
-
// Checkpoint failure is non-fatal
|
|
3121
|
-
}
|
|
3068
|
+
catch { /* non-fatal */ }
|
|
3122
3069
|
}
|
|
3123
3070
|
// ContinuousReflection: 매 5 iteration마다 비상 체크포인트 트리거
|
|
3124
3071
|
// (정기 타이머 외에 iteration 기반 추가 안전망)
|
|
@@ -3128,9 +3075,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
3128
3075
|
try {
|
|
3129
3076
|
await this.continuousReflection.emergencyCheckpoint();
|
|
3130
3077
|
}
|
|
3131
|
-
catch {
|
|
3132
|
-
// reflection 체크포인트 실패는 치명적이지 않음
|
|
3133
|
-
}
|
|
3078
|
+
catch { /* non-fatal */ }
|
|
3134
3079
|
}
|
|
3135
3080
|
// complex/massive task + 다중 변경 파일일 때만 aggregate impact hint 1회 주입
|
|
3136
3081
|
if (!this.impactHintInjected &&
|
|
@@ -3139,6 +3084,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
3139
3084
|
(this.currentComplexity === "complex" || this.currentComplexity === "massive")) {
|
|
3140
3085
|
await this.maybeInjectAggregateImpactHint();
|
|
3141
3086
|
}
|
|
3087
|
+
this.flushEphemeralHints();
|
|
3142
3088
|
// 예산 초과 체크
|
|
3143
3089
|
if (this.tokenUsage.total >= this.config.loop.totalTokenBudget) {
|
|
3144
3090
|
return {
|
|
@@ -3297,7 +3243,245 @@ export class AgentLoop extends EventEmitter {
|
|
|
3297
3243
|
const args = this.parseToolArgs(toolCall.arguments);
|
|
3298
3244
|
const allDefinitions = [...this.config.loop.tools, ...this.mcpToolDefinitions, SPAWN_SUB_AGENT_TOOL];
|
|
3299
3245
|
const matchedDefinition = allDefinitions.find((t) => t.name === toolCall.name);
|
|
3300
|
-
//
|
|
3246
|
+
// Security gate — deterministic pattern check (shell injection, credential leaks, etc.)
|
|
3247
|
+
{
|
|
3248
|
+
const secResult = securityCheck(toolCall.name, args);
|
|
3249
|
+
if (secResult.verdict === "BLOCK") {
|
|
3250
|
+
this.ephemeralHints.push(`[SECURITY] Blocked: ${secResult.reason}`);
|
|
3251
|
+
// Log security block event to .yuan/logs/security-events.jsonl (non-fatal)
|
|
3252
|
+
try {
|
|
3253
|
+
const { mkdirSync, appendFileSync } = await import("node:fs");
|
|
3254
|
+
const logDir = pathJoin(this.config.loop.projectPath, ".yuan", "logs");
|
|
3255
|
+
mkdirSync(logDir, { recursive: true });
|
|
3256
|
+
const logPath = pathJoin(logDir, "security-events.jsonl");
|
|
3257
|
+
appendFileSync(logPath, JSON.stringify({
|
|
3258
|
+
tool: toolCall.name,
|
|
3259
|
+
verdict: "BLOCK",
|
|
3260
|
+
reason: secResult.reason,
|
|
3261
|
+
pattern: secResult.pattern,
|
|
3262
|
+
timestamp: Date.now(),
|
|
3263
|
+
}) + "\n");
|
|
3264
|
+
}
|
|
3265
|
+
catch { /* non-fatal */ }
|
|
3266
|
+
const blockResult = {
|
|
3267
|
+
tool_call_id: toolCall.id,
|
|
3268
|
+
name: toolCall.name,
|
|
3269
|
+
output: `[SECURITY_BLOCKED] ${secResult.reason}. This operation is not allowed.`,
|
|
3270
|
+
success: false,
|
|
3271
|
+
durationMs: 0,
|
|
3272
|
+
};
|
|
3273
|
+
return { result: blockResult, deferredFixPrompt: null };
|
|
3274
|
+
}
|
|
3275
|
+
if (secResult.verdict === "WARN") {
|
|
3276
|
+
this.ephemeralHints.push(`[SECURITY WARNING] ${secResult.reason} — proceeding with caution`);
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
// Workspace Mutation Policy — path-level safety zones (after security gate)
|
|
3280
|
+
if (this.mutationPolicy && (toolCall.name === "file_edit" || toolCall.name === "file_write")) {
|
|
3281
|
+
const filePath = String(args.path ?? args.file_path ?? "");
|
|
3282
|
+
const mutation = this.mutationPolicy.check(filePath);
|
|
3283
|
+
if (!mutation.allowed) {
|
|
3284
|
+
return {
|
|
3285
|
+
result: {
|
|
3286
|
+
tool_call_id: toolCall.id,
|
|
3287
|
+
name: toolCall.name,
|
|
3288
|
+
output: `[MUTATION_BLOCKED] ${mutation.reason}`,
|
|
3289
|
+
success: false,
|
|
3290
|
+
durationMs: 0,
|
|
3291
|
+
},
|
|
3292
|
+
deferredFixPrompt: null,
|
|
3293
|
+
};
|
|
3294
|
+
}
|
|
3295
|
+
if (mutation.requiresApproval) {
|
|
3296
|
+
this.ephemeralHints.push(`[MUTATION] ${filePath} is in ${mutation.zone} zone — approval recommended`);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
// Judgment rules — deterministic rule-based tool approval (loaded from .yuan/judgment-rules.json)
|
|
3300
|
+
if (this.judgmentRegistry) {
|
|
3301
|
+
const judgment = this.judgmentRegistry.evaluate(toolCall.name, args);
|
|
3302
|
+
if (judgment.action === "BLOCK") {
|
|
3303
|
+
const judgmentResult = {
|
|
3304
|
+
tool_call_id: toolCall.id,
|
|
3305
|
+
name: toolCall.name,
|
|
3306
|
+
output: `[JUDGMENT_BLOCKED] ${judgment.reason}`,
|
|
3307
|
+
success: false,
|
|
3308
|
+
durationMs: 0,
|
|
3309
|
+
};
|
|
3310
|
+
return { result: judgmentResult, deferredFixPrompt: null };
|
|
3311
|
+
}
|
|
3312
|
+
if (judgment.action === "WARN") {
|
|
3313
|
+
this.ephemeralHints.push(`[JUDGMENT] ${judgment.reason}`);
|
|
3314
|
+
}
|
|
3315
|
+
if (judgment.action === "REQUIRE_APPROVAL") {
|
|
3316
|
+
this.ephemeralHints.push(`[JUDGMENT] ${judgment.reason} — approval recommended`);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
// ToolGate enforcement — block tools based on Decision Engine gate level
|
|
3320
|
+
{
|
|
3321
|
+
const gate = this.decision.core.toolGate;
|
|
3322
|
+
if (gate.blockedTools.includes(toolCall.name)) {
|
|
3323
|
+
const gateResult = {
|
|
3324
|
+
tool_call_id: toolCall.id,
|
|
3325
|
+
name: toolCall.name,
|
|
3326
|
+
output: `[TOOL_GATE] ${toolCall.name} is blocked in ${gate.level} mode.`,
|
|
3327
|
+
success: false,
|
|
3328
|
+
durationMs: 0,
|
|
3329
|
+
};
|
|
3330
|
+
return { result: gateResult, deferredFixPrompt: null };
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
{
|
|
3334
|
+
const vetoFlags = this.decision.core.vetoFlags;
|
|
3335
|
+
// editVetoed → block file_edit/file_write tools
|
|
3336
|
+
if (vetoFlags.editVetoed && (toolCall.name === "file_edit" || toolCall.name === "file_write")) {
|
|
3337
|
+
const vetoResult = {
|
|
3338
|
+
tool_call_id: toolCall.id,
|
|
3339
|
+
name: toolCall.name,
|
|
3340
|
+
output: "[Decision Engine] Edit vetoed: patch risk is too high without a plan. Please create a plan first or ask the user for clarification.",
|
|
3341
|
+
success: false,
|
|
3342
|
+
durationMs: 0,
|
|
3343
|
+
};
|
|
3344
|
+
return { result: vetoResult, deferredFixPrompt: null };
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
// Pre-edit quality gate: remind LLM to verify before writing
|
|
3348
|
+
{
|
|
3349
|
+
const cq = this.decision.core.codeQuality;
|
|
3350
|
+
if (cq.preEditVerify && (toolCall.name === "file_edit" || toolCall.name === "file_write")) {
|
|
3351
|
+
if (this.toolUsageCounter.edits === 0) {
|
|
3352
|
+
this.ephemeralHints.push(`[Code Quality] This is a ${cq.codeTaskType} task. ` +
|
|
3353
|
+
`Primary risk: ${cq.primaryRisk}. ` +
|
|
3354
|
+
`Verify your change is complete and correct before proceeding to the next file.`);
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
// Dependency guard — detect package installs and manifest changes
|
|
3359
|
+
{
|
|
3360
|
+
const depResult = checkDependencyChange(toolCall.name, args);
|
|
3361
|
+
if (depResult.isDependencyChange) {
|
|
3362
|
+
this.ephemeralHints.push(`[DEP CHANGE] ${depResult.reason}. Verify after: ${depResult.verifyAfter}`);
|
|
3363
|
+
if (depResult.requiresApproval) {
|
|
3364
|
+
this.ephemeralHints.push(`[DEP APPROVAL] Dependency change requires careful review. Budget multiplier: ${depResult.budgetMultiplier}x`);
|
|
3365
|
+
}
|
|
3366
|
+
if (depResult.kind) {
|
|
3367
|
+
const depFilePath = String(args.path ?? args.file_path ?? args.command ?? "");
|
|
3368
|
+
this.emitEvent({
|
|
3369
|
+
kind: "agent:dependency_change_detected",
|
|
3370
|
+
depKind: depResult.kind,
|
|
3371
|
+
path: depFilePath.slice(0, 200),
|
|
3372
|
+
requiresApproval: depResult.requiresApproval,
|
|
3373
|
+
});
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
{
|
|
3378
|
+
const budget = this.decision.core.toolBudget;
|
|
3379
|
+
const counter = this.toolUsageCounter;
|
|
3380
|
+
const toolName = toolCall.name;
|
|
3381
|
+
if (toolName === "file_read")
|
|
3382
|
+
counter.reads++;
|
|
3383
|
+
else if (toolName === "file_edit" || toolName === "file_write") {
|
|
3384
|
+
counter.edits++;
|
|
3385
|
+
const filePath = String(args.path ?? args.file ?? "");
|
|
3386
|
+
if (filePath) {
|
|
3387
|
+
const prev = counter.sameFileEdits.get(filePath) ?? 0;
|
|
3388
|
+
counter.sameFileEdits.set(filePath, prev + 1);
|
|
3389
|
+
if (prev + 1 > budget.maxSameFileEdits) {
|
|
3390
|
+
this.emitEvent({
|
|
3391
|
+
kind: "agent:budget_warning",
|
|
3392
|
+
tool: toolName,
|
|
3393
|
+
used: prev + 1,
|
|
3394
|
+
limit: budget.maxSameFileEdits,
|
|
3395
|
+
});
|
|
3396
|
+
this.contextManager.addMessage({
|
|
3397
|
+
role: "system",
|
|
3398
|
+
content: `[Budget Warning] Same-file edit limit approaching for ${filePath} (${prev + 1}/${budget.maxSameFileEdits}). Consider a different approach.`,
|
|
3399
|
+
});
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
else if (toolName === "shell_exec" || toolName === "bash") {
|
|
3404
|
+
counter.shells++;
|
|
3405
|
+
const cmd = String(args.command ?? "").toLowerCase();
|
|
3406
|
+
if (/\b(test|jest|vitest|mocha|pytest|cargo test|go test)\b/.test(cmd)) {
|
|
3407
|
+
counter.tests++;
|
|
3408
|
+
}
|
|
3409
|
+
// CommandPlanCompiler — validate LLM-proposed shell commands against deterministic compiled plan
|
|
3410
|
+
const rawCmd = String(args.command ?? "");
|
|
3411
|
+
const classified = classifyCommand(rawCmd);
|
|
3412
|
+
if (classified.purpose && this.config.loop.projectPath) {
|
|
3413
|
+
const compilerInput = {
|
|
3414
|
+
verifyDepth: this.decision.core.verifyDepth,
|
|
3415
|
+
packageManager: this.worldState?.deps?.packageManager ?? "npm",
|
|
3416
|
+
testFramework: "unknown",
|
|
3417
|
+
buildTool: "tsc",
|
|
3418
|
+
hasStrictMode: false,
|
|
3419
|
+
monorepo: false,
|
|
3420
|
+
};
|
|
3421
|
+
let compiled;
|
|
3422
|
+
if (classified.purpose === "verify")
|
|
3423
|
+
compiled = compileVerifyCommands(compilerInput);
|
|
3424
|
+
else if (classified.purpose === "build")
|
|
3425
|
+
compiled = compileVerifyCommands({ ...compilerInput, verifyDepth: "thorough" });
|
|
3426
|
+
else
|
|
3427
|
+
compiled = null;
|
|
3428
|
+
if (compiled && compiled.commands.length > 0) {
|
|
3429
|
+
const validation = validateProposedCommand(rawCmd, compiled);
|
|
3430
|
+
if (validation.recommendation === "use_compiled") {
|
|
3431
|
+
this.ephemeralHints.push(`[CMD_PLAN] Command mismatch: ${validation.deviation}. Compiled: ${compiled.commands[0]}`);
|
|
3432
|
+
}
|
|
3433
|
+
else if (validation.recommendation === "warn") {
|
|
3434
|
+
this.ephemeralHints.push(`[CMD_PLAN] Note: ${validation.deviation ?? "Minor deviation from compiled command"}`);
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
else if (toolName === "grep" || toolName === "glob" || toolName === "file_search")
|
|
3440
|
+
counter.searches++;
|
|
3441
|
+
else if (toolName === "web_search" || toolName === "parallel_web_search")
|
|
3442
|
+
counter.webLookups++;
|
|
3443
|
+
else if (toolName === "test_run" || toolName === "run_tests")
|
|
3444
|
+
counter.tests++;
|
|
3445
|
+
const checks = [
|
|
3446
|
+
{ name: "file_read", used: counter.reads, limit: budget.maxFileReads },
|
|
3447
|
+
{ name: "file_edit", used: counter.edits, limit: budget.maxEdits },
|
|
3448
|
+
{ name: "shell_exec", used: counter.shells, limit: budget.maxShellExecs },
|
|
3449
|
+
{ name: "search", used: counter.searches, limit: budget.maxSearches },
|
|
3450
|
+
{ name: "web_lookup", used: counter.webLookups, limit: budget.maxWebLookups },
|
|
3451
|
+
{ name: "test_run", used: counter.tests, limit: budget.maxTestRuns },
|
|
3452
|
+
];
|
|
3453
|
+
for (const check of checks) {
|
|
3454
|
+
if (check.limit <= 0)
|
|
3455
|
+
continue;
|
|
3456
|
+
const ratio = check.used / check.limit;
|
|
3457
|
+
if (ratio >= 1.0) {
|
|
3458
|
+
this.emitEvent({ kind: "agent:budget_exceeded", tool: check.name });
|
|
3459
|
+
const budgetResult = {
|
|
3460
|
+
tool_call_id: toolCall.id,
|
|
3461
|
+
name: toolCall.name,
|
|
3462
|
+
output: `[Decision Engine] Tool budget exceeded for ${check.name} (${check.used}/${check.limit}). Try a different approach or wrap up.`,
|
|
3463
|
+
success: false,
|
|
3464
|
+
durationMs: 0,
|
|
3465
|
+
};
|
|
3466
|
+
return { result: budgetResult, deferredFixPrompt: null };
|
|
3467
|
+
}
|
|
3468
|
+
if (ratio >= 0.8) {
|
|
3469
|
+
this.emitEvent({
|
|
3470
|
+
kind: "agent:budget_warning",
|
|
3471
|
+
tool: check.name,
|
|
3472
|
+
used: check.used,
|
|
3473
|
+
limit: check.limit,
|
|
3474
|
+
});
|
|
3475
|
+
if (this.iterationSystemMsgCount < 5) {
|
|
3476
|
+
this.contextManager.addMessage({
|
|
3477
|
+
role: "system",
|
|
3478
|
+
content: `[Budget Warning] ${check.name} usage at ${Math.round(ratio * 100)}% (${check.used}/${check.limit}). Consider wrapping up or using fewer ${check.name} calls.`,
|
|
3479
|
+
});
|
|
3480
|
+
this.iterationSystemMsgCount++;
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3301
3485
|
try {
|
|
3302
3486
|
this.governor.validateToolCall(toolCall);
|
|
3303
3487
|
}
|
|
@@ -3307,7 +3491,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
3307
3491
|
if (approvalResult) {
|
|
3308
3492
|
return { result: approvalResult, deferredFixPrompt: null };
|
|
3309
3493
|
}
|
|
3310
|
-
//
|
|
3494
|
+
// Approved — continue execution
|
|
3311
3495
|
}
|
|
3312
3496
|
// Generic tool-definition approval gate
|
|
3313
3497
|
if (matchedDefinition?.requiresApproval) {
|
|
@@ -3336,7 +3520,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
3336
3520
|
throw err;
|
|
3337
3521
|
}
|
|
3338
3522
|
}
|
|
3339
|
-
// ApprovalManager: 추가 승인 체크
|
|
3340
3523
|
const approvalRequest = this.approvalManager.checkApproval(toolCall.name, args);
|
|
3341
3524
|
if (approvalRequest) {
|
|
3342
3525
|
const approvalResult = await this.handleApprovalRequest(toolCall, approvalRequest);
|
|
@@ -3344,7 +3527,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
3344
3527
|
return { result: approvalResult, deferredFixPrompt: null };
|
|
3345
3528
|
}
|
|
3346
3529
|
}
|
|
3347
|
-
// Plugin Tool Approval Gate
|
|
3348
3530
|
const pluginTools = this.pluginRegistry.getAllTools();
|
|
3349
3531
|
const matchedPluginTool = pluginTools.find((pt) => pt.tool.name === toolCall.name);
|
|
3350
3532
|
if (matchedPluginTool &&
|
|
@@ -3365,12 +3547,10 @@ export class AgentLoop extends EventEmitter {
|
|
|
3365
3547
|
return { result: pluginApprovalResult, deferredFixPrompt: null };
|
|
3366
3548
|
}
|
|
3367
3549
|
}
|
|
3368
|
-
// spawn_sub_agent 도구 호출 — SubAgent로 위임
|
|
3369
3550
|
if (toolCall.name === "spawn_sub_agent") {
|
|
3370
3551
|
const subAgentResult = await this.executeSpawnSubAgent(toolCall, args);
|
|
3371
3552
|
return { result: subAgentResult, deferredFixPrompt: null };
|
|
3372
3553
|
}
|
|
3373
|
-
// MCP 도구 호출 확인
|
|
3374
3554
|
if (this.mcpClient && this.isMCPTool(toolCall.name)) {
|
|
3375
3555
|
// Emit tool_start before execution (required for trace, QA pipeline, replay)
|
|
3376
3556
|
this.emitEvent({
|
|
@@ -3380,7 +3560,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
3380
3560
|
source: "mcp",
|
|
3381
3561
|
});
|
|
3382
3562
|
const mcpResult = await this.executeMCPTool(toolCall);
|
|
3383
|
-
// Normalize MCP result: wrap search tool output into structured JSON if applicable
|
|
3384
3563
|
const normalizedOutput = this.normalizeMcpResult(toolCall.name, mcpResult.output);
|
|
3385
3564
|
const finalResult = { ...mcpResult, output: normalizedOutput };
|
|
3386
3565
|
this.emitEvent({
|
|
@@ -3394,7 +3573,43 @@ export class AgentLoop extends EventEmitter {
|
|
|
3394
3573
|
this.emitEvent({ kind: "agent:reasoning_delta", text: `tool finished: ${toolCall.name}` });
|
|
3395
3574
|
return { result: finalResult, deferredFixPrompt: null };
|
|
3396
3575
|
}
|
|
3397
|
-
//
|
|
3576
|
+
// Pre-write validator — quality gate before actual file write (after all gates)
|
|
3577
|
+
if ((toolCall.name === "file_edit" || toolCall.name === "file_write") && this.decision.core.codeQuality.strictMode) {
|
|
3578
|
+
const filePath = String(args.path ?? args.file_path ?? "");
|
|
3579
|
+
const content = String(args.content ?? args.new_string ?? "");
|
|
3580
|
+
if (content) {
|
|
3581
|
+
const fileRole = detectFileRole(filePath);
|
|
3582
|
+
const validation = validateBeforeWrite(content, { path: filePath, fileRole, changedHunksOnly: true });
|
|
3583
|
+
if (!validation.valid) {
|
|
3584
|
+
this.ephemeralHints.push(`[QUALITY_BLOCK] ${validation.blockedReason}: ${validation.issues.filter(i => i.severity === "error").map(i => i.message).join("; ")}`);
|
|
3585
|
+
// Record weakness patterns for learning
|
|
3586
|
+
if (this.weaknessTracker) {
|
|
3587
|
+
for (const issue of validation.issues.filter(i => i.severity === "error")) {
|
|
3588
|
+
const pattern = issue.message.includes("TODO") ? "todo_in_code"
|
|
3589
|
+
: issue.message.includes("empty function") ? "empty_function_body"
|
|
3590
|
+
: issue.message.includes("any") ? "any_type_usage"
|
|
3591
|
+
: issue.message.includes("console") ? "console_log_leak"
|
|
3592
|
+
: issue.message.includes("stub") ? "stub_implementation"
|
|
3593
|
+
: "unknown_quality_issue";
|
|
3594
|
+
this.weaknessTracker.record(pattern, `CRITICAL: ${issue.message}. Fix this BEFORE writing.`);
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
return {
|
|
3598
|
+
result: {
|
|
3599
|
+
tool_call_id: toolCall.id,
|
|
3600
|
+
name: toolCall.name,
|
|
3601
|
+
output: `[QUALITY_BLOCKED] ${validation.blockedReason}. Fix these issues and try again.`,
|
|
3602
|
+
success: false,
|
|
3603
|
+
durationMs: 0,
|
|
3604
|
+
},
|
|
3605
|
+
deferredFixPrompt: null,
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3608
|
+
if (validation.issues.length > 0) {
|
|
3609
|
+
this.ephemeralHints.push(`[QUALITY_WARN] ${validation.issues.map(i => i.message).join("; ")}`);
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3398
3613
|
const startTime = Date.now();
|
|
3399
3614
|
const toolAbort = new AbortController();
|
|
3400
3615
|
this.interruptManager.registerToolAbort(toolAbort);
|
|
@@ -3403,6 +3618,10 @@ export class AgentLoop extends EventEmitter {
|
|
|
3403
3618
|
args.file;
|
|
3404
3619
|
if (candidatePath) {
|
|
3405
3620
|
const filePathStr = String(candidatePath);
|
|
3621
|
+
// Record before snapshot in patch journal for atomic rollback
|
|
3622
|
+
if (this.patchJournal) {
|
|
3623
|
+
this.patchJournal.recordBefore(toolCall.name, filePathStr);
|
|
3624
|
+
}
|
|
3406
3625
|
if (!this.originalSnapshots.has(filePathStr)) {
|
|
3407
3626
|
try {
|
|
3408
3627
|
const { readFile } = await import("node:fs/promises");
|
|
@@ -3416,7 +3635,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
3416
3635
|
}
|
|
3417
3636
|
}
|
|
3418
3637
|
}
|
|
3419
|
-
// Emit search_start for web search tools so TUI can show search progress
|
|
3420
3638
|
const isSearchTool = toolCall.name === "web_search" || toolCall.name === "parallel_web_search";
|
|
3421
3639
|
if (isSearchTool) {
|
|
3422
3640
|
const searchArgs = typeof toolCall.arguments === "string"
|
|
@@ -3437,7 +3655,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
3437
3655
|
try {
|
|
3438
3656
|
const result = await this.toolExecutor.execute(toolCall, toolAbort?.signal);
|
|
3439
3657
|
this.interruptManager.clearToolAbort();
|
|
3440
|
-
// Emit search_result after completion
|
|
3441
3658
|
if (isSearchTool && result.success) {
|
|
3442
3659
|
const searchArgs = typeof toolCall.arguments === "string"
|
|
3443
3660
|
? (() => { try {
|
|
@@ -3484,7 +3701,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
3484
3701
|
});
|
|
3485
3702
|
this.reasoningTree.add("tool", `success: ${toolCall.name}`);
|
|
3486
3703
|
if (["file_write", "file_edit"].includes(toolCall.name) && result.success) {
|
|
3487
|
-
// Phase transition: explore → implement on first write
|
|
3488
3704
|
if (this.taskPhase === "explore") {
|
|
3489
3705
|
this.transitionPhase("implement", `first write: ${toolCall.name}`);
|
|
3490
3706
|
}
|
|
@@ -3494,45 +3710,79 @@ export class AgentLoop extends EventEmitter {
|
|
|
3494
3710
|
const filePathStr = String(filePath);
|
|
3495
3711
|
if (!this.changedFiles.includes(filePathStr)) {
|
|
3496
3712
|
this.changedFiles.push(filePathStr);
|
|
3497
|
-
// Cap changedFiles to prevent unbounded growth on massive refactors
|
|
3498
3713
|
if (this.changedFiles.length > BOUNDS.changedFiles) {
|
|
3499
3714
|
this.changedFiles = this.changedFiles.slice(-BOUNDS.changedFiles);
|
|
3500
3715
|
}
|
|
3501
3716
|
}
|
|
3502
|
-
// Task 2: track write tool paths per-iteration for QA triggering
|
|
3503
3717
|
if (!this.iterationWriteToolPaths.includes(filePathStr)) {
|
|
3504
3718
|
this.iterationWriteToolPaths.push(filePathStr);
|
|
3505
3719
|
}
|
|
3506
|
-
// Task 3: track TS/TSX files modified this iteration for auto-tsc
|
|
3507
3720
|
if (filePathStr.match(/\.[cm]?tsx?$/) && !this.iterationTsFilesModified.includes(filePathStr)) {
|
|
3508
3721
|
this.iterationTsFilesModified.push(filePathStr);
|
|
3509
3722
|
}
|
|
3510
3723
|
this.emitEvent({ kind: "agent:file_change", path: filePathStr, diff: result.output });
|
|
3511
|
-
// Emit evidence report (async, fire-and-forget — no loop complexity added)
|
|
3512
3724
|
this.emitEvidenceReport(filePathStr, toolCall.name).catch(() => { });
|
|
3513
|
-
//
|
|
3725
|
+
// SemanticDiffReviewer — classify change meaning and recommend verification depth
|
|
3726
|
+
if (this.originalSnapshots.has(filePathStr)) {
|
|
3727
|
+
try {
|
|
3728
|
+
const { readFile } = await import("node:fs/promises");
|
|
3729
|
+
const newContent = await readFile(filePathStr, "utf-8");
|
|
3730
|
+
const oldContent = this.originalSnapshots.get(filePathStr) ?? "";
|
|
3731
|
+
const review = reviewFileDiff({
|
|
3732
|
+
path: filePathStr,
|
|
3733
|
+
oldContent,
|
|
3734
|
+
newContent,
|
|
3735
|
+
language: filePathStr.match(/\.[cm]?tsx?$/) ? "typescript" : undefined,
|
|
3736
|
+
});
|
|
3737
|
+
if (review.recommendedRiskBoost > 0) {
|
|
3738
|
+
this.ephemeralHints.push(`[SEMANTIC_DIFF] ${filePathStr}: ${review.changes.join(", ")} — risk +${review.recommendedRiskBoost.toFixed(2)}, recommend verify=${review.recommendedVerifyDepth}`);
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
catch { /* non-fatal — file may have been deleted */ }
|
|
3742
|
+
}
|
|
3514
3743
|
if (this.config.loop.projectPath) {
|
|
3515
3744
|
const wsProjectPath = this.config.loop.projectPath;
|
|
3516
3745
|
new WorldStateCollector({ projectPath: wsProjectPath, skipTest: true })
|
|
3517
3746
|
.collect()
|
|
3518
3747
|
.then((snapshot) => { this.worldState = snapshot; })
|
|
3519
|
-
.catch(() => {
|
|
3520
|
-
// Non-fatal: world state update failure should not interrupt tool execution
|
|
3521
|
-
});
|
|
3748
|
+
.catch(() => { });
|
|
3522
3749
|
}
|
|
3523
3750
|
if (this.impactAnalyzer) {
|
|
3524
|
-
this.analyzeFileImpact(filePathStr).catch((
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3751
|
+
this.analyzeFileImpact(filePathStr).catch(() => { });
|
|
3752
|
+
}
|
|
3753
|
+
// Auto-create rollback point based on Decision failureSurface
|
|
3754
|
+
if (this.patchJournal && this.patchJournal.shouldCreateRollbackPoint(this.decision.core.failureSurface.patchRisk, this.changedFiles.length)) {
|
|
3755
|
+
const point = this.patchJournal.createRollbackPoint(`patchRisk=${this.decision.core.failureSurface.patchRisk}, files=${this.changedFiles.length}`);
|
|
3756
|
+
this.emitEvent({
|
|
3757
|
+
kind: "agent:rollback_point_created",
|
|
3758
|
+
pointId: point.id,
|
|
3759
|
+
reason: point.reason,
|
|
3528
3760
|
});
|
|
3529
3761
|
}
|
|
3762
|
+
// Patch scope tracking (after successful file mutation)
|
|
3763
|
+
if (this.patchScopeController) {
|
|
3764
|
+
const content = String(args.content ?? args.new_string ?? "");
|
|
3765
|
+
const isProtected = this.mutationPolicy?.check(filePathStr).zone === "PROTECTED" || this.mutationPolicy?.check(filePathStr).zone === "CAUTION";
|
|
3766
|
+
this.patchScopeController.recordChange(filePathStr, content.split("\n").length, 0, isProtected ?? false);
|
|
3767
|
+
const scopeCheck = this.patchScopeController.check();
|
|
3768
|
+
if (!scopeCheck.allowed) {
|
|
3769
|
+
this.ephemeralHints.push(`[SCOPE_LIMIT] ${scopeCheck.reason}. Consider splitting into smaller tasks or requesting plan escalation.`);
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
// Verifier rules — deterministic check of tool output for success/failure patterns
|
|
3774
|
+
{
|
|
3775
|
+
const verification = verifyToolResult(toolCall.name, args, result.output, result.success);
|
|
3776
|
+
if (verification.verdict === "FAIL") {
|
|
3777
|
+
this.ephemeralHints.push(`[VERIFY FAIL] ${verification.reason}. Suggested: ${verification.suggestedAction}`);
|
|
3778
|
+
}
|
|
3779
|
+
if (verification.verdict === "WARN") {
|
|
3780
|
+
this.ephemeralHints.push(`[VERIFY WARN] ${verification.reason}`);
|
|
3781
|
+
}
|
|
3530
3782
|
}
|
|
3531
|
-
// StateUpdater: sync world model with actual tool execution result
|
|
3532
3783
|
if (this.stateUpdater && result.success) {
|
|
3533
3784
|
this.stateUpdater.applyToolResult(toolCall.name, args, result).catch(() => { });
|
|
3534
3785
|
}
|
|
3535
|
-
// Extract edited file path so auto-fix can lint only the changed file
|
|
3536
3786
|
const editedFilePath = args?.path
|
|
3537
3787
|
?? args?.file_path
|
|
3538
3788
|
?? undefined;
|
|
@@ -3853,10 +4103,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
3853
4103
|
};
|
|
3854
4104
|
}
|
|
3855
4105
|
}
|
|
3856
|
-
/**
|
|
3857
|
-
* Governor의 ApprovalRequiredError를 ApprovalManager로 처리.
|
|
3858
|
-
* 승인되면 null 반환 (실행 계속), 거부되면 ToolResult 반환 (실행 차단).
|
|
3859
|
-
*/
|
|
3860
4106
|
async handleApproval(toolCall, args, err) {
|
|
3861
4107
|
const request = {
|
|
3862
4108
|
id: randomUUID(),
|
|
@@ -3896,20 +4142,15 @@ export class AgentLoop extends EventEmitter {
|
|
|
3896
4142
|
* 실패 시 수정 프롬프트를 대화 히스토리에 추가.
|
|
3897
4143
|
*/
|
|
3898
4144
|
async validateAndFeedback(toolName, result, filePath) {
|
|
3899
|
-
// file_write/file_edit/shell_exec만 검증
|
|
3900
4145
|
if (!["file_write", "file_edit", "shell_exec"].includes(toolName)) {
|
|
3901
4146
|
return null;
|
|
3902
4147
|
}
|
|
3903
4148
|
const validation = await this.autoFixLoop.validateResult(toolName, result.output, result.success, this.config.loop.projectPath, filePath);
|
|
3904
4149
|
if (validation.passed) {
|
|
3905
|
-
// 검증 통과 → 수정 시도 기록 초기화
|
|
3906
4150
|
this.autoFixLoop.resetAttempts();
|
|
3907
4151
|
return null;
|
|
3908
4152
|
}
|
|
3909
|
-
// 검증 실패 → 에러 피드백
|
|
3910
4153
|
if (!this.autoFixLoop.canRetry()) {
|
|
3911
|
-
// 재시도 한도 초과 → 에러 event emit + reset + 에이전트에게 피드백 반환
|
|
3912
|
-
// (return null 대신 피드백 프롬프트를 반환하여 에이전트가 다른 접근법 시도 가능)
|
|
3913
4154
|
const failMsg = validation.failures
|
|
3914
4155
|
.map((f) => `[${f.type}] ${f.message}`)
|
|
3915
4156
|
.join("; ");
|
|
@@ -3920,14 +4161,11 @@ export class AgentLoop extends EventEmitter {
|
|
|
3920
4161
|
this.autoFixLoop.resetAttempts();
|
|
3921
4162
|
return `[LINT VALIDATION FAILED after 3 auto-fix attempts — SKIP lint for this file and continue with the main task. Do NOT retry fixing lint errors. Move on to the next step.]\nFailures: ${failMsg}`;
|
|
3922
4163
|
}
|
|
3923
|
-
// 수정 프롬프트 생성 — 히스토리 추가는 caller가 tool result 추가 후 수행
|
|
3924
4164
|
const errorMsg = validation.failures
|
|
3925
4165
|
.map((f) => `[${f.type}] ${f.message}\n${f.rawOutput}`)
|
|
3926
4166
|
.join("\n\n");
|
|
3927
4167
|
const fixPrompt = this.autoFixLoop.buildFixPrompt(errorMsg, `After ${toolName} execution on project at ${this.config.loop.projectPath}`);
|
|
3928
|
-
// 수정 시도 기록
|
|
3929
4168
|
this.autoFixLoop.recordAttempt(errorMsg, "Requesting LLM fix", false, 0);
|
|
3930
|
-
// fixPrompt를 반환 — caller가 tool result 메시지 추가 후 context에 넣음
|
|
3931
4169
|
return fixPrompt;
|
|
3932
4170
|
}
|
|
3933
4171
|
/**
|
|
@@ -3948,11 +4186,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
3948
4186
|
}
|
|
3949
4187
|
return args;
|
|
3950
4188
|
}
|
|
3951
|
-
// ─── Continuation Helpers ───
|
|
3952
|
-
/**
|
|
3953
|
-
* 토큰 예산 소진 임박 시 자동 체크포인트를 저장한다.
|
|
3954
|
-
* 현재 진행 상태, 변경 파일, 에러 등을 직렬화.
|
|
3955
|
-
*/
|
|
3956
4189
|
async saveAutoCheckpoint(iteration) {
|
|
3957
4190
|
if (!this.continuationEngine)
|
|
3958
4191
|
return;
|
|
@@ -3985,14 +4218,8 @@ export class AgentLoop extends EventEmitter {
|
|
|
3985
4218
|
});
|
|
3986
4219
|
}
|
|
3987
4220
|
}
|
|
3988
|
-
catch {
|
|
3989
|
-
// 체크포인트 저장 실패는 치명적이지 않음
|
|
3990
|
-
}
|
|
4221
|
+
catch { /* non-fatal */ }
|
|
3991
4222
|
}
|
|
3992
|
-
/**
|
|
3993
|
-
* 매 iteration 시작 시 현재 플랜 진행 상황을 컨텍스트에 주입.
|
|
3994
|
-
* 같은 태스크 인덱스라면 3 iteration마다만 주입 (컨텍스트 bloat 방지).
|
|
3995
|
-
*/
|
|
3996
4223
|
injectPlanProgress(iteration) {
|
|
3997
4224
|
if (!this.activePlan)
|
|
3998
4225
|
return;
|
|
@@ -4030,16 +4257,8 @@ export class AgentLoop extends EventEmitter {
|
|
|
4030
4257
|
else {
|
|
4031
4258
|
lines.push(`\nAll tasks complete — verify and wrap up.`);
|
|
4032
4259
|
}
|
|
4033
|
-
this.
|
|
4034
|
-
role: "system",
|
|
4035
|
-
content: lines.join("\n"),
|
|
4036
|
-
});
|
|
4037
|
-
this.iterationSystemMsgCount++;
|
|
4260
|
+
this.ephemeralHints.push(lines.join("\n"));
|
|
4038
4261
|
}
|
|
4039
|
-
/**
|
|
4040
|
-
* 현재 태스크의 targetFiles가 changedFiles에 포함됐는지 확인해
|
|
4041
|
-
* 완료 감지 시 다음 태스크로 자동 전진.
|
|
4042
|
-
*/
|
|
4043
4262
|
tryAdvancePlanTask() {
|
|
4044
4263
|
if (!this.activePlan)
|
|
4045
4264
|
return;
|
|
@@ -4148,12 +4367,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
4148
4367
|
getWorldState() {
|
|
4149
4368
|
return this.worldState;
|
|
4150
4369
|
}
|
|
4151
|
-
// ─── Interrupt Helpers ───
|
|
4152
|
-
/**
|
|
4153
|
-
* InterruptManager 이벤트를 AgentLoop에 연결한다.
|
|
4154
|
-
* - soft interrupt: 피드백을 user 메시지로 주입
|
|
4155
|
-
* - hard interrupt: 루프 즉시 중단
|
|
4156
|
-
*/
|
|
4157
4370
|
setupInterruptListeners() {
|
|
4158
4371
|
// soft interrupt: 피드백을 대화 히스토리에 주입
|
|
4159
4372
|
this.interruptManager.on("interrupt:feedback", (feedback) => {
|
|
@@ -4167,10 +4380,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
4167
4380
|
this.aborted = true;
|
|
4168
4381
|
});
|
|
4169
4382
|
}
|
|
4170
|
-
/**
|
|
4171
|
-
* pause 상태일 때 resume 시그널을 대기한다.
|
|
4172
|
-
* resume 또는 hard interrupt가 올 때까지 블로킹.
|
|
4173
|
-
*/
|
|
4174
4383
|
waitForResume() {
|
|
4175
4384
|
return new Promise((resolve) => {
|
|
4176
4385
|
// 이미 resume 상태이면 즉시 반환
|
|
@@ -4192,11 +4401,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
4192
4401
|
this.interruptManager.on("interrupt:hard", onHard);
|
|
4193
4402
|
});
|
|
4194
4403
|
}
|
|
4195
|
-
// ─── Impact Analysis ───
|
|
4196
|
-
/**
|
|
4197
|
-
* Returns a cached ImpactReport if the same files were already analyzed,
|
|
4198
|
-
* otherwise computes and caches a fresh report.
|
|
4199
|
-
*/
|
|
4200
4404
|
async getOrComputeImpact(files) {
|
|
4201
4405
|
if (!this.impactAnalyzer || files.length === 0)
|
|
4202
4406
|
return null;
|
|
@@ -4229,12 +4433,8 @@ export class AgentLoop extends EventEmitter {
|
|
|
4229
4433
|
kind: "agent:thinking",
|
|
4230
4434
|
content: `Impact analysis: ${report.riskLevel} risk. ${report.affectedFiles.length} affected files, ${report.breakingChanges.length} breaking changes.`,
|
|
4231
4435
|
});
|
|
4232
|
-
// 고위험 변경 정보를 LLM에 주입
|
|
4233
4436
|
const impactPrompt = this.impactAnalyzer.formatForPrompt(report);
|
|
4234
|
-
this.
|
|
4235
|
-
role: "system",
|
|
4236
|
-
content: impactPrompt,
|
|
4237
|
-
});
|
|
4437
|
+
this.ephemeralHints.push(impactPrompt);
|
|
4238
4438
|
}
|
|
4239
4439
|
}
|
|
4240
4440
|
/**
|
|
@@ -4269,10 +4469,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
4269
4469
|
if (planPreview) {
|
|
4270
4470
|
hintLines.push("", "Suggested refactor order:", planPreview);
|
|
4271
4471
|
}
|
|
4272
|
-
this.
|
|
4273
|
-
role: "system",
|
|
4274
|
-
content: hintLines.join("\n"),
|
|
4275
|
-
});
|
|
4472
|
+
this.ephemeralHints.push(hintLines.join("\n"));
|
|
4276
4473
|
this.impactHintInjected = true;
|
|
4277
4474
|
this.emitEvent({
|
|
4278
4475
|
kind: "agent:thinking",
|
|
@@ -4281,14 +4478,8 @@ export class AgentLoop extends EventEmitter {
|
|
|
4281
4478
|
`${report.deadCodeCandidates.length} dead code candidates.`,
|
|
4282
4479
|
});
|
|
4283
4480
|
}
|
|
4284
|
-
catch {
|
|
4285
|
-
// aggregate impact 실패는 치명적이지 않음
|
|
4286
|
-
}
|
|
4481
|
+
catch { /* non-fatal */ }
|
|
4287
4482
|
}
|
|
4288
|
-
/**
|
|
4289
|
-
* 종료 직전 최종 impact 요약 생성.
|
|
4290
|
-
* assistant final summary에만 붙이고 system prompt 오염은 하지 않는다.
|
|
4291
|
-
*/
|
|
4292
4483
|
async buildFinalImpactSummary() {
|
|
4293
4484
|
if (!this.impactAnalyzer || this.changedFiles.length === 0)
|
|
4294
4485
|
return null;
|
|
@@ -4340,14 +4531,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
4340
4531
|
const args = this.parseToolArgs(toolCall.arguments);
|
|
4341
4532
|
return this.mcpClient.callToolAsYuan(toolCall.name, args, toolCall.id);
|
|
4342
4533
|
}
|
|
4343
|
-
/**
|
|
4344
|
-
* Normalize an MCP tool result for consistent downstream consumption.
|
|
4345
|
-
*
|
|
4346
|
-
* - Search tools (tool name contains "search"): if the output is unstructured text,
|
|
4347
|
-
* wrap it into a JSON array of `{ title, url, snippet, source }` objects.
|
|
4348
|
-
* Each non-empty line is treated as a snippet entry when structured data is absent.
|
|
4349
|
-
* - All other tools: pass through unchanged.
|
|
4350
|
-
*/
|
|
4351
4534
|
normalizeMcpResult(toolName, output) {
|
|
4352
4535
|
const isSearch = /search/i.test(toolName);
|
|
4353
4536
|
if (!isSearch)
|
|
@@ -4378,11 +4561,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
4378
4561
|
});
|
|
4379
4562
|
return JSON.stringify(structured, null, 2);
|
|
4380
4563
|
}
|
|
4381
|
-
/**
|
|
4382
|
-
* ContinuousReflection overflow signal을 soft rollover로 처리한다.
|
|
4383
|
-
* 절대 abort하지 않고, 체크포인트 저장 후 다음 iteration에서
|
|
4384
|
-
* ContextManager가 압축된 컨텍스트를 사용하도록 둔다.
|
|
4385
|
-
*/
|
|
4386
4564
|
async handleSoftContextOverflow() {
|
|
4387
4565
|
try {
|
|
4388
4566
|
await this.saveAutoCheckpoint(this.iterationCount);
|
|
@@ -4409,7 +4587,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
4409
4587
|
}
|
|
4410
4588
|
this.traceRecorder?.stop();
|
|
4411
4589
|
}
|
|
4412
|
-
// ─── Helpers ───
|
|
4413
4590
|
/**
|
|
4414
4591
|
* Builds a Map<filePath, toolOutput> for all changed files from write/edit tool results.
|
|
4415
4592
|
* Used by selfReflection deepVerify and quickVerify.
|
|
@@ -4463,11 +4640,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
4463
4640
|
});
|
|
4464
4641
|
return { reason: "ERROR", error: message };
|
|
4465
4642
|
}
|
|
4466
|
-
// ─── Task phase transitions ───────────────────────────────────────────────
|
|
4467
|
-
/**
|
|
4468
|
-
* Deterministic phase transition — no LLM involved.
|
|
4469
|
-
* Emits agent:phase_transition event for TUI trace.
|
|
4470
|
-
*/
|
|
4471
4643
|
transitionPhase(to, trigger) {
|
|
4472
4644
|
if (this.taskPhase === to)
|
|
4473
4645
|
return;
|
|
@@ -4481,10 +4653,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
4481
4653
|
trigger,
|
|
4482
4654
|
});
|
|
4483
4655
|
}
|
|
4484
|
-
/**
|
|
4485
|
-
* Cheap local checks run in verify phase — no LLM.
|
|
4486
|
-
* Returns true if all checks pass (safe to advance to finalize).
|
|
4487
|
-
*/
|
|
4488
4656
|
async runCheapChecks() {
|
|
4489
4657
|
const projectPath = this.config.loop.projectPath;
|
|
4490
4658
|
// 1. Diff size check — if nothing changed, skip
|
|
@@ -4517,16 +4685,10 @@ export class AgentLoop extends EventEmitter {
|
|
|
4517
4685
|
if (hasErrors) {
|
|
4518
4686
|
// cheap check: tsc errors found — staying in verify
|
|
4519
4687
|
// Inject TS errors so LLM sees them and fixes them
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
this.contextManager.addMessage({
|
|
4525
|
-
role: "system",
|
|
4526
|
-
content: `[Verify Phase] TypeScript errors found:\n\`\`\`\n${truncated}\n\`\`\`\nPlease fix before completion.`,
|
|
4527
|
-
});
|
|
4528
|
-
this.iterationSystemMsgCount++;
|
|
4529
|
-
}
|
|
4688
|
+
const truncated = result.output.length > 1000
|
|
4689
|
+
? result.output.slice(0, 1000) + "\n[truncated]"
|
|
4690
|
+
: result.output;
|
|
4691
|
+
this.ephemeralHints.push(`[Verify Phase] TypeScript errors found:\n\`\`\`\n${truncated}\n\`\`\`\nPlease fix before completion.`);
|
|
4530
4692
|
return false;
|
|
4531
4693
|
}
|
|
4532
4694
|
}
|
|
@@ -4537,11 +4699,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
4537
4699
|
}
|
|
4538
4700
|
return true;
|
|
4539
4701
|
}
|
|
4540
|
-
// ─── Evidence Report ───────────────────────────────────────────────────────
|
|
4541
|
-
/**
|
|
4542
|
-
* After a file write/edit, collect diff stats + syntax signal and emit
|
|
4543
|
-
* `agent:evidence_report`. Runs async and never blocks the main loop.
|
|
4544
|
-
*/
|
|
4545
4702
|
async emitEvidenceReport(filePath, tool) {
|
|
4546
4703
|
const timestamp = Date.now();
|
|
4547
4704
|
const projectPath = this.config.loop.projectPath;
|
|
@@ -4565,10 +4722,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
4565
4722
|
}
|
|
4566
4723
|
}
|
|
4567
4724
|
catch { /* non-fatal */ }
|
|
4568
|
-
// 2. Syntax check — skipped here to avoid spawning a duplicate tsc process.
|
|
4569
|
-
// runCheapChecks() already runs tsc at the implement→verify boundary and
|
|
4570
|
-
// the result flows to the QA pipeline. Spawning tsc per-write would
|
|
4571
|
-
// create N parallel tsc processes for bulk refactors.
|
|
4572
4725
|
const syntax = "skipped";
|
|
4573
4726
|
this.emitEvent({
|
|
4574
4727
|
kind: "agent:evidence_report",
|
|
@@ -4579,13 +4732,11 @@ export class AgentLoop extends EventEmitter {
|
|
|
4579
4732
|
lintResult: "skipped",
|
|
4580
4733
|
timestamp,
|
|
4581
4734
|
});
|
|
4582
|
-
// If a dep-graph file changed, invalidate arch summary cache
|
|
4583
4735
|
const isDepsChange = filePath.includes("package.json") || filePath.includes("tsconfig");
|
|
4584
4736
|
if (isDepsChange && this.archSummarizer) {
|
|
4585
4737
|
this.archSummarizer.regenerate().catch(() => { });
|
|
4586
4738
|
}
|
|
4587
4739
|
}
|
|
4588
|
-
// ─── OverheadGovernor helpers ─────────────────────────────────────────────
|
|
4589
4740
|
/**
|
|
4590
4741
|
* 현재 런타임 상태로 TriggerContext 빌드.
|
|
4591
4742
|
*/
|
|
@@ -4624,15 +4775,8 @@ export class AgentLoop extends EventEmitter {
|
|
|
4624
4775
|
this.repeatedErrorSignature = undefined;
|
|
4625
4776
|
}
|
|
4626
4777
|
}
|
|
4627
|
-
// ─── Message pruning — GPT recommendation ────────────────────────────────
|
|
4628
4778
|
static MAX_MESSAGES = 40;
|
|
4629
4779
|
static PRUNE_KEEP_RECENT = 10;
|
|
4630
|
-
/**
|
|
4631
|
-
* 메시지 배열이 MAX_MESSAGES를 초과하면 prune.
|
|
4632
|
-
* 구조: [system] + [task summary] + [last PRUNE_KEEP_RECENT turns]
|
|
4633
|
-
*
|
|
4634
|
-
* "turns" = user/assistant 쌍 (tool messages는 해당 assistant 메시지에 귀속)
|
|
4635
|
-
*/
|
|
4636
4780
|
pruneMessagesIfNeeded() {
|
|
4637
4781
|
const msgs = this.contextManager.getMessages();
|
|
4638
4782
|
if (msgs.length <= AgentLoop.MAX_MESSAGES)
|