@yuaone/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +663 -0
- package/README.md +15 -0
- package/dist/__tests__/context-manager.test.d.ts +6 -0
- package/dist/__tests__/context-manager.test.d.ts.map +1 -0
- package/dist/__tests__/context-manager.test.js +220 -0
- package/dist/__tests__/context-manager.test.js.map +1 -0
- package/dist/__tests__/governor.test.d.ts +6 -0
- package/dist/__tests__/governor.test.d.ts.map +1 -0
- package/dist/__tests__/governor.test.js +210 -0
- package/dist/__tests__/governor.test.js.map +1 -0
- package/dist/__tests__/model-router.test.d.ts +6 -0
- package/dist/__tests__/model-router.test.d.ts.map +1 -0
- package/dist/__tests__/model-router.test.js +329 -0
- package/dist/__tests__/model-router.test.js.map +1 -0
- package/dist/agent-logger.d.ts +384 -0
- package/dist/agent-logger.d.ts.map +1 -0
- package/dist/agent-logger.js +820 -0
- package/dist/agent-logger.js.map +1 -0
- package/dist/agent-loop.d.ts +163 -0
- package/dist/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop.js +609 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/agent-modes.d.ts +85 -0
- package/dist/agent-modes.d.ts.map +1 -0
- package/dist/agent-modes.js +418 -0
- package/dist/agent-modes.js.map +1 -0
- package/dist/approval.d.ts +137 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +299 -0
- package/dist/approval.js.map +1 -0
- package/dist/async-completion-queue.d.ts +56 -0
- package/dist/async-completion-queue.d.ts.map +1 -0
- package/dist/async-completion-queue.js +77 -0
- package/dist/async-completion-queue.js.map +1 -0
- package/dist/auto-fix.d.ts +174 -0
- package/dist/auto-fix.d.ts.map +1 -0
- package/dist/auto-fix.js +319 -0
- package/dist/auto-fix.js.map +1 -0
- package/dist/codebase-context.d.ts +396 -0
- package/dist/codebase-context.d.ts.map +1 -0
- package/dist/codebase-context.js +1260 -0
- package/dist/codebase-context.js.map +1 -0
- package/dist/conflict-resolver.d.ts +191 -0
- package/dist/conflict-resolver.d.ts.map +1 -0
- package/dist/conflict-resolver.js +524 -0
- package/dist/conflict-resolver.js.map +1 -0
- package/dist/constants.d.ts +52 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +141 -0
- package/dist/constants.js.map +1 -0
- package/dist/context-budget.d.ts +435 -0
- package/dist/context-budget.d.ts.map +1 -0
- package/dist/context-budget.js +903 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/context-compressor.d.ts +143 -0
- package/dist/context-compressor.d.ts.map +1 -0
- package/dist/context-compressor.js +511 -0
- package/dist/context-compressor.js.map +1 -0
- package/dist/context-manager.d.ts +112 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +247 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/continuous-reflection.d.ts +267 -0
- package/dist/continuous-reflection.d.ts.map +1 -0
- package/dist/continuous-reflection.js +338 -0
- package/dist/continuous-reflection.js.map +1 -0
- package/dist/cross-file-refactor.d.ts +352 -0
- package/dist/cross-file-refactor.d.ts.map +1 -0
- package/dist/cross-file-refactor.js +1544 -0
- package/dist/cross-file-refactor.js.map +1 -0
- package/dist/dag-orchestrator.d.ts +138 -0
- package/dist/dag-orchestrator.d.ts.map +1 -0
- package/dist/dag-orchestrator.js +379 -0
- package/dist/dag-orchestrator.js.map +1 -0
- package/dist/debate-orchestrator.d.ts +301 -0
- package/dist/debate-orchestrator.d.ts.map +1 -0
- package/dist/debate-orchestrator.js +719 -0
- package/dist/debate-orchestrator.js.map +1 -0
- package/dist/dependency-analyzer.d.ts +113 -0
- package/dist/dependency-analyzer.d.ts.map +1 -0
- package/dist/dependency-analyzer.js +444 -0
- package/dist/dependency-analyzer.js.map +1 -0
- package/dist/design-loop.d.ts +59 -0
- package/dist/design-loop.d.ts.map +1 -0
- package/dist/design-loop.js +344 -0
- package/dist/design-loop.js.map +1 -0
- package/dist/doc-intelligence.d.ts +383 -0
- package/dist/doc-intelligence.d.ts.map +1 -0
- package/dist/doc-intelligence.js +1307 -0
- package/dist/doc-intelligence.js.map +1 -0
- package/dist/dynamic-role-generator.d.ts +76 -0
- package/dist/dynamic-role-generator.d.ts.map +1 -0
- package/dist/dynamic-role-generator.js +194 -0
- package/dist/dynamic-role-generator.js.map +1 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +102 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-bus.d.ts +159 -0
- package/dist/event-bus.d.ts.map +1 -0
- package/dist/event-bus.js +305 -0
- package/dist/event-bus.js.map +1 -0
- package/dist/execution-engine.d.ts +425 -0
- package/dist/execution-engine.d.ts.map +1 -0
- package/dist/execution-engine.js +1555 -0
- package/dist/execution-engine.js.map +1 -0
- package/dist/git-intelligence.d.ts +306 -0
- package/dist/git-intelligence.d.ts.map +1 -0
- package/dist/git-intelligence.js +1099 -0
- package/dist/git-intelligence.js.map +1 -0
- package/dist/governor.d.ts +77 -0
- package/dist/governor.d.ts.map +1 -0
- package/dist/governor.js +161 -0
- package/dist/governor.js.map +1 -0
- package/dist/hierarchical-planner.d.ts +313 -0
- package/dist/hierarchical-planner.d.ts.map +1 -0
- package/dist/hierarchical-planner.js +981 -0
- package/dist/hierarchical-planner.js.map +1 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/dist/intent-inference.d.ts +103 -0
- package/dist/intent-inference.d.ts.map +1 -0
- package/dist/intent-inference.js +605 -0
- package/dist/intent-inference.js.map +1 -0
- package/dist/interrupt-manager.d.ts +143 -0
- package/dist/interrupt-manager.d.ts.map +1 -0
- package/dist/interrupt-manager.js +196 -0
- package/dist/interrupt-manager.js.map +1 -0
- package/dist/kernel.d.ts +564 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +1419 -0
- package/dist/kernel.js.map +1 -0
- package/dist/language-support.d.ts +232 -0
- package/dist/language-support.d.ts.map +1 -0
- package/dist/language-support.js +1134 -0
- package/dist/language-support.js.map +1 -0
- package/dist/llm-client.d.ts +82 -0
- package/dist/llm-client.d.ts.map +1 -0
- package/dist/llm-client.js +475 -0
- package/dist/llm-client.js.map +1 -0
- package/dist/mcp-client.d.ts +232 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +718 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/memory-manager.d.ts +200 -0
- package/dist/memory-manager.d.ts.map +1 -0
- package/dist/memory-manager.js +568 -0
- package/dist/memory-manager.js.map +1 -0
- package/dist/memory.d.ts +87 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +341 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-router.d.ts +245 -0
- package/dist/model-router.d.ts.map +1 -0
- package/dist/model-router.js +632 -0
- package/dist/model-router.js.map +1 -0
- package/dist/parallel-executor.d.ts +125 -0
- package/dist/parallel-executor.d.ts.map +1 -0
- package/dist/parallel-executor.js +201 -0
- package/dist/parallel-executor.js.map +1 -0
- package/dist/perf-optimizer.d.ts +212 -0
- package/dist/perf-optimizer.d.ts.map +1 -0
- package/dist/perf-optimizer.js +721 -0
- package/dist/perf-optimizer.js.map +1 -0
- package/dist/persona.d.ts +305 -0
- package/dist/persona.d.ts.map +1 -0
- package/dist/persona.js +887 -0
- package/dist/persona.js.map +1 -0
- package/dist/planner.d.ts +70 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +264 -0
- package/dist/planner.js.map +1 -0
- package/dist/qa-pipeline.d.ts +365 -0
- package/dist/qa-pipeline.d.ts.map +1 -0
- package/dist/qa-pipeline.js +1352 -0
- package/dist/qa-pipeline.js.map +1 -0
- package/dist/reasoning-adapter.d.ts +116 -0
- package/dist/reasoning-adapter.d.ts.map +1 -0
- package/dist/reasoning-adapter.js +187 -0
- package/dist/reasoning-adapter.js.map +1 -0
- package/dist/role-registry.d.ts +55 -0
- package/dist/role-registry.d.ts.map +1 -0
- package/dist/role-registry.js +192 -0
- package/dist/role-registry.js.map +1 -0
- package/dist/sandbox-tiers.d.ts +327 -0
- package/dist/sandbox-tiers.d.ts.map +1 -0
- package/dist/sandbox-tiers.js +928 -0
- package/dist/sandbox-tiers.js.map +1 -0
- package/dist/security-scanner.d.ts +222 -0
- package/dist/security-scanner.d.ts.map +1 -0
- package/dist/security-scanner.js +1129 -0
- package/dist/security-scanner.js.map +1 -0
- package/dist/security.d.ts +93 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +393 -0
- package/dist/security.js.map +1 -0
- package/dist/self-reflection.d.ts +397 -0
- package/dist/self-reflection.d.ts.map +1 -0
- package/dist/self-reflection.js +908 -0
- package/dist/self-reflection.js.map +1 -0
- package/dist/session-persistence.d.ts +191 -0
- package/dist/session-persistence.d.ts.map +1 -0
- package/dist/session-persistence.js +395 -0
- package/dist/session-persistence.js.map +1 -0
- package/dist/speculative-executor.d.ts +210 -0
- package/dist/speculative-executor.d.ts.map +1 -0
- package/dist/speculative-executor.js +618 -0
- package/dist/speculative-executor.js.map +1 -0
- package/dist/state-machine.d.ts +289 -0
- package/dist/state-machine.d.ts.map +1 -0
- package/dist/state-machine.js +695 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/sub-agent.d.ts +177 -0
- package/dist/sub-agent.d.ts.map +1 -0
- package/dist/sub-agent.js +303 -0
- package/dist/sub-agent.js.map +1 -0
- package/dist/system-prompt.d.ts +26 -0
- package/dist/system-prompt.d.ts.map +1 -0
- package/dist/system-prompt.js +84 -0
- package/dist/system-prompt.js.map +1 -0
- package/dist/test-intelligence.d.ts +439 -0
- package/dist/test-intelligence.d.ts.map +1 -0
- package/dist/test-intelligence.js +1165 -0
- package/dist/test-intelligence.js.map +1 -0
- package/dist/types.d.ts +632 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/vector-index.d.ts +314 -0
- package/dist/vector-index.d.ts.map +1 -0
- package/dist/vector-index.js +618 -0
- package/dist/vector-index.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,1555 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module execution-engine
|
|
3
|
+
* @description Top-level Execution Engine — StateMachine(brain) ↔ AgentLoop(hands) 오케스트레이터.
|
|
4
|
+
*
|
|
5
|
+
* 모든 하위 시스템을 연결하여 사용자 목표를 end-to-end로 실행한다:
|
|
6
|
+
* - **StateMachine** — phase 전이 (analyze → plan → implement → verify → done)
|
|
7
|
+
* - **AgentLoop** — LLM ↔ Tool 반복 실행 (implement/fix phase)
|
|
8
|
+
* - **HierarchicalPlanner** — 3-level 계획 수립 (plan phase)
|
|
9
|
+
* - **SelfReflection** — 6-dimension 심층 검증 (verify phase)
|
|
10
|
+
* - **CodebaseContext** — 코드베이스 인덱싱/검색 (analyze phase)
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const engine = new ExecutionEngine({
|
|
15
|
+
* byokConfig: { provider: "anthropic", apiKey: "sk-..." },
|
|
16
|
+
* projectPath: "/my/project",
|
|
17
|
+
* toolExecutor: myExecutor,
|
|
18
|
+
* maxIterations: 100,
|
|
19
|
+
* totalTokenBudget: 500_000,
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* engine.on("phase:enter", (phase) => console.log(`Phase: ${phase}`));
|
|
23
|
+
* engine.on("monologue", (entry) => console.log(entry.thought));
|
|
24
|
+
*
|
|
25
|
+
* const result = await engine.execute("Add error handling to all API routes");
|
|
26
|
+
* console.log(result.success, result.summary);
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
import { EventEmitter } from "node:events";
|
|
30
|
+
import { readFile } from "node:fs/promises";
|
|
31
|
+
import { BYOKClient } from "./llm-client.js";
|
|
32
|
+
import { AgentLoop } from "./agent-loop.js";
|
|
33
|
+
import { AgentLogger } from "./agent-logger.js";
|
|
34
|
+
import { AgentStateMachine, } from "./state-machine.js";
|
|
35
|
+
import { CodebaseContext } from "./codebase-context.js";
|
|
36
|
+
import { VectorIndex } from "./vector-index.js";
|
|
37
|
+
import { LanguageSupport } from "./language-support.js";
|
|
38
|
+
import { HierarchicalPlanner, } from "./hierarchical-planner.js";
|
|
39
|
+
import { SelfReflection, } from "./self-reflection.js";
|
|
40
|
+
import { PlanGraphManager } from "./kernel.js";
|
|
41
|
+
import { MCPClient } from "./mcp-client.js";
|
|
42
|
+
import { PerfOptimizer } from "./perf-optimizer.js";
|
|
43
|
+
import { SandboxManager } from "./sandbox-tiers.js";
|
|
44
|
+
import { DebateOrchestrator } from "./debate-orchestrator.js";
|
|
45
|
+
import { SecurityScanner } from "./security-scanner.js";
|
|
46
|
+
import { DocIntelligence } from "./doc-intelligence.js";
|
|
47
|
+
import { IntentInferenceEngine } from "./intent-inference.js";
|
|
48
|
+
import { SpeculativeExecutor } from "./speculative-executor.js";
|
|
49
|
+
// ─── Defaults ────────────────────────────────────────────────────
|
|
50
|
+
const ENGINE_DEFAULTS = {
|
|
51
|
+
maxIterations: 100,
|
|
52
|
+
totalTokenBudget: 500_000,
|
|
53
|
+
maxFixAttempts: 3,
|
|
54
|
+
enableCodebaseIndex: true,
|
|
55
|
+
enableHierarchicalPlanning: true,
|
|
56
|
+
enableDeepVerify: true,
|
|
57
|
+
enableMonologue: true,
|
|
58
|
+
enableLearning: true,
|
|
59
|
+
skipDesignForSimple: true,
|
|
60
|
+
enableParallel: true,
|
|
61
|
+
enableParallelExecution: true,
|
|
62
|
+
maxParallelAgents: 3,
|
|
63
|
+
};
|
|
64
|
+
// ─── ExecutionEngine ─────────────────────────────────────────────
|
|
65
|
+
/**
|
|
66
|
+
* ExecutionEngine — YUAN 에이전트의 최상위 오케스트레이터.
|
|
67
|
+
*
|
|
68
|
+
* StateMachine이 phase 전이를 결정하고,
|
|
69
|
+
* ExecutionEngine이 각 phase에서 적절한 하위 시스템을 호출한다.
|
|
70
|
+
*
|
|
71
|
+
* - **analyze**: CodebaseContext로 프로젝트 구조 파악
|
|
72
|
+
* - **design**: LLM으로 접근법 제안
|
|
73
|
+
* - **plan**: HierarchicalPlanner로 3-level 계획 수립
|
|
74
|
+
* - **implement**: AgentLoop로 LLM ↔ Tool 반복 실행
|
|
75
|
+
* - **verify**: SelfReflection.deepVerify로 6-dimension 검증
|
|
76
|
+
* - **fix**: AgentLoop로 에러 수정
|
|
77
|
+
* - **replan**: HierarchicalPlanner.replan으로 재계획
|
|
78
|
+
*/
|
|
79
|
+
export class ExecutionEngine extends EventEmitter {
|
|
80
|
+
config;
|
|
81
|
+
llmClient;
|
|
82
|
+
reflection;
|
|
83
|
+
codebaseContext;
|
|
84
|
+
vectorIndex;
|
|
85
|
+
stateMachine;
|
|
86
|
+
abortController;
|
|
87
|
+
changedFiles;
|
|
88
|
+
originalFiles;
|
|
89
|
+
lastTermination;
|
|
90
|
+
lastVerifyResult;
|
|
91
|
+
hierarchicalPlan;
|
|
92
|
+
planGraph;
|
|
93
|
+
sessionId;
|
|
94
|
+
_logger;
|
|
95
|
+
sessionPersistence;
|
|
96
|
+
/** DAG 기반 병렬 실행 결과 캐시 (stepIndex → StepResult) */
|
|
97
|
+
parallelStepResults;
|
|
98
|
+
/** DAG 기반 병렬 실행이 완료되었는지 */
|
|
99
|
+
parallelExecutionDone;
|
|
100
|
+
/** MCP server configurations (stored from constructor config) */
|
|
101
|
+
mcpServerConfigs;
|
|
102
|
+
/** MCP Client — external tool bridge (null if disabled) */
|
|
103
|
+
mcpClient = null;
|
|
104
|
+
/** MCP tool definitions discovered at runtime */
|
|
105
|
+
mcpToolDefinitions = [];
|
|
106
|
+
/** Performance optimizer (null if disabled) */
|
|
107
|
+
perfOptimizer = null;
|
|
108
|
+
/** Sandbox manager for tool call validation (null if disabled) */
|
|
109
|
+
sandboxManager = null;
|
|
110
|
+
constructor(config) {
|
|
111
|
+
super();
|
|
112
|
+
this.config = {
|
|
113
|
+
byokConfig: config.byokConfig,
|
|
114
|
+
projectPath: config.projectPath,
|
|
115
|
+
toolExecutor: config.toolExecutor,
|
|
116
|
+
maxIterations: config.maxIterations ?? ENGINE_DEFAULTS.maxIterations,
|
|
117
|
+
totalTokenBudget: config.totalTokenBudget ?? ENGINE_DEFAULTS.totalTokenBudget,
|
|
118
|
+
maxFixAttempts: config.maxFixAttempts ?? ENGINE_DEFAULTS.maxFixAttempts,
|
|
119
|
+
enableCodebaseIndex: config.enableCodebaseIndex ?? ENGINE_DEFAULTS.enableCodebaseIndex,
|
|
120
|
+
enableHierarchicalPlanning: config.enableHierarchicalPlanning ?? ENGINE_DEFAULTS.enableHierarchicalPlanning,
|
|
121
|
+
enableDeepVerify: config.enableDeepVerify ?? ENGINE_DEFAULTS.enableDeepVerify,
|
|
122
|
+
enableMonologue: config.enableMonologue ?? ENGINE_DEFAULTS.enableMonologue,
|
|
123
|
+
enableLearning: config.enableLearning ?? ENGINE_DEFAULTS.enableLearning,
|
|
124
|
+
skipDesignForSimple: config.skipDesignForSimple ?? ENGINE_DEFAULTS.skipDesignForSimple,
|
|
125
|
+
enableParallel: config.enableParallel ?? ENGINE_DEFAULTS.enableParallel,
|
|
126
|
+
enableParallelExecution: config.enableParallelExecution ?? ENGINE_DEFAULTS.enableParallelExecution,
|
|
127
|
+
maxParallelAgents: config.maxParallelAgents ?? ENGINE_DEFAULTS.maxParallelAgents,
|
|
128
|
+
enableVectorSearch: config.enableVectorSearch ?? false,
|
|
129
|
+
enableDebate: config.enableDebate ?? false,
|
|
130
|
+
enableSecurityScan: config.enableSecurityScan ?? false,
|
|
131
|
+
enableDocGeneration: config.enableDocGeneration ?? false,
|
|
132
|
+
enableIntentInference: config.enableIntentInference ?? true,
|
|
133
|
+
enableSpeculative: config.enableSpeculative ?? false,
|
|
134
|
+
speculativeMaxApproaches: config.speculativeMaxApproaches ?? 3,
|
|
135
|
+
governorConfig: config.governorConfig,
|
|
136
|
+
contextConfig: config.contextConfig,
|
|
137
|
+
approvalConfig: config.approvalConfig,
|
|
138
|
+
approvalHandler: config.approvalHandler,
|
|
139
|
+
autoFixConfig: config.autoFixConfig,
|
|
140
|
+
};
|
|
141
|
+
this.llmClient = new BYOKClient(this.config.byokConfig);
|
|
142
|
+
this.sessionId = config.sessionId ?? `engine-${Date.now().toString(36)}`;
|
|
143
|
+
this.sessionPersistence = config.sessionPersistence ?? null;
|
|
144
|
+
this.reflection = new SelfReflection(this.sessionId, {
|
|
145
|
+
enableDeepVerify: this.config.enableDeepVerify,
|
|
146
|
+
enableMonologue: this.config.enableMonologue,
|
|
147
|
+
enableLearning: this.config.enableLearning,
|
|
148
|
+
});
|
|
149
|
+
// Initialize structured logger
|
|
150
|
+
this._logger = config.logger ?? new AgentLogger({
|
|
151
|
+
sessionId: this.sessionId,
|
|
152
|
+
level: "info",
|
|
153
|
+
outputs: [{ type: "memory" }],
|
|
154
|
+
...config.loggerConfig,
|
|
155
|
+
});
|
|
156
|
+
this.codebaseContext = null;
|
|
157
|
+
this.vectorIndex = null;
|
|
158
|
+
this.stateMachine = null;
|
|
159
|
+
this.planGraph = null;
|
|
160
|
+
this.abortController = new AbortController();
|
|
161
|
+
this.changedFiles = new Set();
|
|
162
|
+
this.originalFiles = new Map();
|
|
163
|
+
this.lastTermination = { reason: "USER_CANCELLED" };
|
|
164
|
+
this.parallelStepResults = new Map();
|
|
165
|
+
this.parallelExecutionDone = false;
|
|
166
|
+
// Store MCP server configs for use in execute()
|
|
167
|
+
this.mcpServerConfigs = config.mcpServerConfigs ?? [];
|
|
168
|
+
// Initialize PerfOptimizer if enabled
|
|
169
|
+
if (config.enablePerfTracking) {
|
|
170
|
+
this.perfOptimizer = new PerfOptimizer();
|
|
171
|
+
}
|
|
172
|
+
// Initialize SandboxManager if enabled
|
|
173
|
+
if (config.enableSandbox) {
|
|
174
|
+
this.sandboxManager = new SandboxManager({
|
|
175
|
+
projectPath: this.config.projectPath,
|
|
176
|
+
defaultTier: config.sandboxTier,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
// Wire reflection monologue events to engine events
|
|
180
|
+
this.reflection.on("monologue:entry", (entry) => {
|
|
181
|
+
this.emit("monologue", entry);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// ─── Logger Access ─────────────────────────────────────────────
|
|
185
|
+
/** 실행 로거 — 실행 후 로그 조회에 사용 */
|
|
186
|
+
get logger() {
|
|
187
|
+
return this._logger;
|
|
188
|
+
}
|
|
189
|
+
/** 현재 PlanGraphManager 인스턴스 (계획 진행 상태 조회용) */
|
|
190
|
+
get currentPlanGraph() {
|
|
191
|
+
return this.planGraph;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 현재 계획의 진행 상황을 반환한다.
|
|
195
|
+
* PlanGraphManager가 없으면 null을 반환한다.
|
|
196
|
+
*
|
|
197
|
+
* @returns 진행 상황 객체 또는 null
|
|
198
|
+
*/
|
|
199
|
+
getPlanProgress() {
|
|
200
|
+
if (!this.planGraph)
|
|
201
|
+
return null;
|
|
202
|
+
const progress = this.planGraph.getProgress();
|
|
203
|
+
const graphState = this.planGraph.getState();
|
|
204
|
+
return {
|
|
205
|
+
completed: progress.completed,
|
|
206
|
+
total: progress.total,
|
|
207
|
+
percent: progress.percent,
|
|
208
|
+
running: [...graphState.runningNodes],
|
|
209
|
+
failed: [...graphState.failedNodes],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
// ─── Main Entry ────────────────────────────────────────────────
|
|
213
|
+
/**
|
|
214
|
+
* 사용자 목표를 end-to-end로 실행한다.
|
|
215
|
+
*
|
|
216
|
+
* 내부적으로 StateMachine을 생성하고, 각 phase에 대한 콜백을 연결하여
|
|
217
|
+
* idle → analyze → plan → implement → verify → done까지 자동 진행한다.
|
|
218
|
+
*
|
|
219
|
+
* @param goal - 사용자의 실행 목표
|
|
220
|
+
* @returns 실행 결과 (성공 여부, 변경 파일, 토큰 사용량 등)
|
|
221
|
+
*/
|
|
222
|
+
async execute(goal) {
|
|
223
|
+
const startTime = Date.now();
|
|
224
|
+
this.abortController = new AbortController();
|
|
225
|
+
this.changedFiles.clear();
|
|
226
|
+
this.originalFiles.clear();
|
|
227
|
+
this.lastVerifyResult = undefined;
|
|
228
|
+
this.hierarchicalPlan = undefined;
|
|
229
|
+
this.planGraph = null;
|
|
230
|
+
this.parallelStepResults = new Map();
|
|
231
|
+
this.parallelExecutionDone = false;
|
|
232
|
+
this.emit("engine:start", goal);
|
|
233
|
+
this._logger.logInput(goal);
|
|
234
|
+
this.reflection.think("start", `Goal received: "${goal}"`);
|
|
235
|
+
try {
|
|
236
|
+
// 0. Intent inference pre-processing (refine ambiguous goals)
|
|
237
|
+
if (this.config.enableIntentInference) {
|
|
238
|
+
try {
|
|
239
|
+
const intentEngine = new IntentInferenceEngine({
|
|
240
|
+
byokConfig: this.config.byokConfig,
|
|
241
|
+
projectPath: this.config.projectPath,
|
|
242
|
+
maxContextTokens: 4000,
|
|
243
|
+
});
|
|
244
|
+
const intentResult = await intentEngine.infer(goal);
|
|
245
|
+
this.emit("intent:inferred", intentResult);
|
|
246
|
+
if (intentResult.isAmbiguous) {
|
|
247
|
+
this._logger.info("system", `Intent inference: ambiguous input refined — "${goal}" → "${intentResult.refinedGoal}" (category: ${intentResult.category}, confidence: ${intentResult.confidence.toFixed(2)})`);
|
|
248
|
+
this.reflection.think("analyze", `Intent inference refined ambiguous goal: "${goal}" → "${intentResult.refinedGoal}" [${intentResult.category}, confidence=${intentResult.confidence.toFixed(2)}]`);
|
|
249
|
+
goal = intentResult.refinedGoal;
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
this._logger.info("system", `Intent inference: input is clear (category: ${intentResult.category}, confidence: ${intentResult.confidence.toFixed(2)})`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (intentErr) {
|
|
256
|
+
this._logger.warn("system", `Intent inference failed (continuing with original goal): ${intentErr instanceof Error ? intentErr.message : String(intentErr)}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// 1. Build codebase index (if enabled and not already built)
|
|
260
|
+
if (this.config.enableCodebaseIndex && !this.codebaseContext) {
|
|
261
|
+
const languageSupport = new LanguageSupport();
|
|
262
|
+
this.codebaseContext = new CodebaseContext(this.config.projectPath, languageSupport);
|
|
263
|
+
this._logger.info("system", "Building codebase index", { projectPath: this.config.projectPath });
|
|
264
|
+
this.reflection.think("analyze", "Building codebase index...");
|
|
265
|
+
await this.codebaseContext.buildIndex();
|
|
266
|
+
const stats = this.codebaseContext.getStats();
|
|
267
|
+
this._logger.info("system", `Codebase indexed: ${stats.totalFiles} files, ${stats.totalSymbols} symbols`);
|
|
268
|
+
this.reflection.think("analyze", `Index built: ${stats.totalFiles} files, ${stats.totalSymbols} symbols`);
|
|
269
|
+
}
|
|
270
|
+
// 1b. Conditional VectorIndex initialization (requires pgvector)
|
|
271
|
+
if (this.config.enableVectorSearch && !this.vectorIndex) {
|
|
272
|
+
// VectorIndex requires a real SQLExecutor and EmbeddingProvider.
|
|
273
|
+
// When no DB is available, create a no-op stub so the engine
|
|
274
|
+
// doesn't crash — vector search simply returns empty results.
|
|
275
|
+
const noopSqlExecutor = {
|
|
276
|
+
query: async () => ({ rows: [] }),
|
|
277
|
+
};
|
|
278
|
+
const noopEmbeddingProvider = {
|
|
279
|
+
embed: async (texts) => texts.map(() => []),
|
|
280
|
+
dimension: 1536,
|
|
281
|
+
};
|
|
282
|
+
this.vectorIndex = new VectorIndex({
|
|
283
|
+
projectId: this.config.projectPath,
|
|
284
|
+
sqlExecutor: noopSqlExecutor,
|
|
285
|
+
embeddingProvider: noopEmbeddingProvider,
|
|
286
|
+
});
|
|
287
|
+
this._logger.info("system", "VectorIndex initialized (no-op stub — supply real DB for pgvector search)");
|
|
288
|
+
}
|
|
289
|
+
// 1c. MCP Client initialization (optional — connect to external MCP servers)
|
|
290
|
+
if (this.mcpServerConfigs.length > 0) {
|
|
291
|
+
try {
|
|
292
|
+
this.mcpClient = new MCPClient({
|
|
293
|
+
servers: this.mcpServerConfigs.map((s) => ({
|
|
294
|
+
name: s.name,
|
|
295
|
+
transport: "stdio",
|
|
296
|
+
command: s.command,
|
|
297
|
+
args: s.args ?? [],
|
|
298
|
+
})),
|
|
299
|
+
});
|
|
300
|
+
await this.mcpClient.connectAll();
|
|
301
|
+
this.mcpToolDefinitions = this.mcpClient.toToolDefinitions();
|
|
302
|
+
this._logger.info("system", `MCP: discovered ${this.mcpToolDefinitions.length} tool(s) from ${this.mcpServerConfigs.length} server(s)`);
|
|
303
|
+
this.reflection.think("analyze", `MCP tools available: ${this.mcpToolDefinitions.length}`);
|
|
304
|
+
}
|
|
305
|
+
catch (mcpErr) {
|
|
306
|
+
this._logger.warn("system", `MCP connection failed (continuing without MCP tools): ${mcpErr instanceof Error ? mcpErr.message : String(mcpErr)}`);
|
|
307
|
+
this.mcpClient = null;
|
|
308
|
+
this.mcpToolDefinitions = [];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// 2. Create state machine with wired callbacks
|
|
312
|
+
const context = this.buildStateMachineContext();
|
|
313
|
+
this.stateMachine = new AgentStateMachine(context, {
|
|
314
|
+
maxFixAttempts: this.config.maxFixAttempts,
|
|
315
|
+
skipDesignForSimple: this.config.skipDesignForSimple,
|
|
316
|
+
enableParallel: this.config.enableParallel,
|
|
317
|
+
});
|
|
318
|
+
// Wire state machine events
|
|
319
|
+
this.stateMachine.on("phase:enter", (phase) => {
|
|
320
|
+
this.emit("phase:enter", phase);
|
|
321
|
+
if (this.perfOptimizer) {
|
|
322
|
+
this.perfOptimizer.startPhase(phase);
|
|
323
|
+
}
|
|
324
|
+
// Checkpoint after each phase transition
|
|
325
|
+
this.checkpoint().catch((err) => {
|
|
326
|
+
console.warn("[YUAN] Checkpoint failed:", err instanceof Error ? err.message : err);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
this.stateMachine.on("phase:exit", (phase) => {
|
|
330
|
+
this.emit("phase:exit", phase);
|
|
331
|
+
if (this.perfOptimizer) {
|
|
332
|
+
this.perfOptimizer.endPhase(phase);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
this.stateMachine.on("verify:result", (r) => {
|
|
336
|
+
// Convert VerifyResult to a lightweight emit; deepVerify result is emitted separately
|
|
337
|
+
this.emit("verify:result", this.lastVerifyResult);
|
|
338
|
+
});
|
|
339
|
+
// 3. Run state machine
|
|
340
|
+
const finalState = await this.stateMachine.run(goal, this.abortController.signal);
|
|
341
|
+
// 4. Build result
|
|
342
|
+
const result = this.buildResult(finalState, startTime);
|
|
343
|
+
// Performance report (if tracking enabled)
|
|
344
|
+
if (this.perfOptimizer) {
|
|
345
|
+
const perfReport = this.perfOptimizer.generateReport(this.sessionId);
|
|
346
|
+
this._logger.info("system", `Perf: efficiency score ${perfReport.efficiencyScore}/100`);
|
|
347
|
+
if (perfReport.bottlenecks.length > 0) {
|
|
348
|
+
this._logger.info("system", `Perf: ${perfReport.bottlenecks.length} bottleneck(s) detected`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Disconnect MCP servers (cleanup)
|
|
352
|
+
if (this.mcpClient) {
|
|
353
|
+
await this.mcpClient.disconnectAll().catch(() => {
|
|
354
|
+
// Best-effort cleanup
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
this._logger.logOutput(result.summary, result.success, result.totalTokens);
|
|
358
|
+
await this._logger.flush();
|
|
359
|
+
this.emit("engine:complete", result);
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
364
|
+
this._logger.error("error", `Execution failed: ${error.message}`);
|
|
365
|
+
await this._logger.flush();
|
|
366
|
+
this.emit("engine:error", error);
|
|
367
|
+
return {
|
|
368
|
+
success: false,
|
|
369
|
+
termination: { reason: "ERROR", error: error.message },
|
|
370
|
+
finalPhase: this.stateMachine?.getState().phase ?? "idle",
|
|
371
|
+
changedFiles: [...this.changedFiles],
|
|
372
|
+
summary: `Execution failed: ${error.message}`,
|
|
373
|
+
totalTokens: { input: 0, output: 0 },
|
|
374
|
+
totalIterations: 0,
|
|
375
|
+
totalToolCalls: 0,
|
|
376
|
+
durationMs: Date.now() - startTime,
|
|
377
|
+
monologue: [...this.reflection.getMonologue()],
|
|
378
|
+
learnings: [...this.reflection.getAllLearnings()],
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* 런타임 상태를 디스크에 체크포인트로 저장한다.
|
|
384
|
+
* sessionPersistence가 없으면 아무것도 하지 않는다.
|
|
385
|
+
* 실패해도 실행을 중단하지 않는다 (경고 로그만 남김).
|
|
386
|
+
*
|
|
387
|
+
* @param stepIndex - 현재 step 인덱스 (있으면)
|
|
388
|
+
*/
|
|
389
|
+
async checkpoint(stepIndex) {
|
|
390
|
+
if (!this.sessionPersistence)
|
|
391
|
+
return;
|
|
392
|
+
try {
|
|
393
|
+
await this.sessionPersistence.saveRuntimeState(this.sessionId, {
|
|
394
|
+
planGraphState: this.planGraph?.toJSON() ?? null,
|
|
395
|
+
stateMachinePhase: this.stateMachine?.getState().phase,
|
|
396
|
+
stepIndex,
|
|
397
|
+
reflectionLearnings: [...this.reflection.getAllLearnings()],
|
|
398
|
+
reflectionMonologue: [...this.reflection.getMonologue()],
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
this._logger.warn("system", `Checkpoint save failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* 실행을 중단한다.
|
|
407
|
+
* 현재 진행 중인 AgentLoop와 StateMachine이 다음 체크포인트에서 중단된다.
|
|
408
|
+
*/
|
|
409
|
+
abort() {
|
|
410
|
+
// Emergency checkpoint save before stopping
|
|
411
|
+
this.checkpoint().catch(() => {
|
|
412
|
+
// Ignore — best-effort save during abort
|
|
413
|
+
});
|
|
414
|
+
this.abortController.abort();
|
|
415
|
+
}
|
|
416
|
+
// ─── StateMachineContext Builder ────────────────────────────────
|
|
417
|
+
/**
|
|
418
|
+
* StateMachine에 주입할 콜백 컨텍스트를 생성한다.
|
|
419
|
+
* 각 콜백이 적절한 하위 시스템(AgentLoop, HierarchicalPlanner 등)을 호출한다.
|
|
420
|
+
*/
|
|
421
|
+
buildStateMachineContext() {
|
|
422
|
+
return {
|
|
423
|
+
analyzeFn: this.analyzeGoal.bind(this),
|
|
424
|
+
designFn: this.designApproaches.bind(this),
|
|
425
|
+
planFn: this.createPlan.bind(this),
|
|
426
|
+
executeFn: this.executeStepWithParallel.bind(this),
|
|
427
|
+
verifyFn: this.verifyWork.bind(this),
|
|
428
|
+
fixFn: this.fixErrors.bind(this),
|
|
429
|
+
replanFn: this.replanWork.bind(this),
|
|
430
|
+
delegateFn: this.delegateToUser.bind(this),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
// ─── Phase Callbacks ───────────────────────────────────────────
|
|
434
|
+
/**
|
|
435
|
+
* analyze phase 콜백 — CodebaseContext + LLM으로 목표를 분석한다.
|
|
436
|
+
*
|
|
437
|
+
* @param goal - 사용자 목표
|
|
438
|
+
* @param state - 현재 에이전트 상태
|
|
439
|
+
* @returns 복잡도 판정과 분석 컨텍스트
|
|
440
|
+
*/
|
|
441
|
+
async analyzeGoal(goal, state) {
|
|
442
|
+
const exitAnalyze = this._logger.enterLayer("analyze", `Analyzing goal: "${goal}"`);
|
|
443
|
+
this.reflection.think("analyze", `Analyzing goal: "${goal}"`);
|
|
444
|
+
let codebaseStats = {};
|
|
445
|
+
if (this.codebaseContext) {
|
|
446
|
+
codebaseStats = this.codebaseContext.getStats();
|
|
447
|
+
// Semantic search for relevant symbols
|
|
448
|
+
const relevant = this.codebaseContext.searchSymbols(goal, 10);
|
|
449
|
+
if (relevant.length > 0) {
|
|
450
|
+
const symbolNames = relevant.map((r) => `${r.symbol.name} (${r.symbol.kind}) in ${r.symbol.file}`);
|
|
451
|
+
this.reflection.think("analyze", `Found ${relevant.length} relevant symbols: ${symbolNames.slice(0, 5).join(", ")}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const analysisPrompt = this.buildAnalysisPrompt(goal, codebaseStats);
|
|
455
|
+
const response = await this.llmClient.chat([
|
|
456
|
+
{ role: "system", content: analysisPrompt },
|
|
457
|
+
{ role: "user", content: goal },
|
|
458
|
+
]);
|
|
459
|
+
const content = response.content ?? "";
|
|
460
|
+
// Parse complexity from LLM response
|
|
461
|
+
let complexity = "moderate";
|
|
462
|
+
const complexityMatch = content.match(/\b(trivial|simple|moderate|complex|massive)\b/i);
|
|
463
|
+
if (complexityMatch) {
|
|
464
|
+
complexity = complexityMatch[1].toLowerCase();
|
|
465
|
+
}
|
|
466
|
+
this._logger.logDecision("Goal complexity assessment", ["trivial", "simple", "moderate", "complex", "massive"], complexity, `LLM analysis determined complexity based on goal and codebase context`);
|
|
467
|
+
this.reflection.think("analyze", `Analysis complete — complexity: ${complexity}`);
|
|
468
|
+
exitAnalyze();
|
|
469
|
+
return { complexity, context: content };
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* design phase 콜백 — LLM에게 구현 접근법 제안을 요청한다.
|
|
473
|
+
*
|
|
474
|
+
* @param goal - 사용자 목표
|
|
475
|
+
* @param context - analyze phase에서 수집한 컨텍스트
|
|
476
|
+
* @returns 제안된 접근법 목록
|
|
477
|
+
*/
|
|
478
|
+
async designApproaches(goal, context) {
|
|
479
|
+
const exitDesign = this._logger.enterLayer("design", "Generating approach options");
|
|
480
|
+
this.reflection.think("design", "Generating approach options...");
|
|
481
|
+
const prompt = `You are a senior architect. Based on the analysis, propose 2-3 implementation approaches.
|
|
482
|
+
|
|
483
|
+
## Analysis Context
|
|
484
|
+
${context}
|
|
485
|
+
|
|
486
|
+
## Goal
|
|
487
|
+
${goal}
|
|
488
|
+
|
|
489
|
+
## Output Format
|
|
490
|
+
Respond with ONLY a JSON array (no markdown fences):
|
|
491
|
+
[
|
|
492
|
+
{
|
|
493
|
+
"id": 1,
|
|
494
|
+
"name": "approach name",
|
|
495
|
+
"description": "what this approach does",
|
|
496
|
+
"pros": ["advantage 1"],
|
|
497
|
+
"cons": ["disadvantage 1"],
|
|
498
|
+
"estimatedComplexity": "low",
|
|
499
|
+
"recommended": true
|
|
500
|
+
}
|
|
501
|
+
]
|
|
502
|
+
|
|
503
|
+
Complexity: "low" | "medium" | "high"
|
|
504
|
+
Exactly one approach should have recommended=true.`;
|
|
505
|
+
const response = await this.llmClient.chat([
|
|
506
|
+
{ role: "system", content: prompt },
|
|
507
|
+
{ role: "user", content: goal },
|
|
508
|
+
]);
|
|
509
|
+
const content = response.content ?? "[]";
|
|
510
|
+
try {
|
|
511
|
+
const jsonStr = this.extractJson(content);
|
|
512
|
+
const parsed = JSON.parse(jsonStr);
|
|
513
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
514
|
+
const recommended = parsed.find((a) => a.recommended);
|
|
515
|
+
this._logger.logDecision("Select implementation approach", parsed.map((a) => a.name), recommended?.name ?? parsed[0].name, `LLM recommended approach based on goal analysis`);
|
|
516
|
+
this.reflection.think("design", `Generated ${parsed.length} approaches. Recommended: ${recommended?.name ?? "none"}`);
|
|
517
|
+
exitDesign();
|
|
518
|
+
return parsed;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
// Parse failure — fall through
|
|
523
|
+
}
|
|
524
|
+
// Default: single direct approach
|
|
525
|
+
const defaultApproach = {
|
|
526
|
+
id: 1,
|
|
527
|
+
name: "direct",
|
|
528
|
+
description: "Direct implementation based on analysis",
|
|
529
|
+
pros: ["Simple", "Fast"],
|
|
530
|
+
cons: ["May miss edge cases"],
|
|
531
|
+
estimatedComplexity: "medium",
|
|
532
|
+
recommended: true,
|
|
533
|
+
};
|
|
534
|
+
this._logger.logReasoning("LLM response could not be parsed, falling back to default direct approach");
|
|
535
|
+
this.reflection.think("design", "Using default direct approach");
|
|
536
|
+
exitDesign();
|
|
537
|
+
return [defaultApproach];
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* plan phase 콜백 — HierarchicalPlanner로 3-level 실행 계획을 수립한다.
|
|
541
|
+
*
|
|
542
|
+
* @param goal - 사용자 목표
|
|
543
|
+
* @param approach - 선택된 접근법
|
|
544
|
+
* @returns 실행 계획 (ExecutionPlan 형식)
|
|
545
|
+
*/
|
|
546
|
+
async createPlan(goal, approach) {
|
|
547
|
+
const exitPlan = this._logger.enterLayer("plan", `Creating plan with approach: "${approach.name}"`);
|
|
548
|
+
this.reflection.think("plan", `Creating plan with approach: "${approach.name}"`);
|
|
549
|
+
if (this.config.enableHierarchicalPlanning) {
|
|
550
|
+
const planner = new HierarchicalPlanner({
|
|
551
|
+
projectPath: this.config.projectPath,
|
|
552
|
+
});
|
|
553
|
+
const hPlan = await planner.createHierarchicalPlan(goal, this.llmClient);
|
|
554
|
+
this.hierarchicalPlan = hPlan;
|
|
555
|
+
this._logger.info("system", `Hierarchical plan: ${hPlan.tactical.length} tasks, ${hPlan.totalEstimatedIterations} iterations, ${hPlan.parallelizableGroups.length} parallel groups`);
|
|
556
|
+
this.reflection.think("plan", `Hierarchical plan created: ${hPlan.tactical.length} tactical tasks, ` +
|
|
557
|
+
`${hPlan.totalEstimatedIterations} estimated iterations, ` +
|
|
558
|
+
`${hPlan.parallelizableGroups.length} parallel groups`);
|
|
559
|
+
const executionPlan = planner.toExecutionPlan(hPlan);
|
|
560
|
+
// Initialize PlanGraphManager for step progress tracking
|
|
561
|
+
this.planGraph = PlanGraphManager.fromExecutionPlan(this.sessionId, executionPlan);
|
|
562
|
+
this._logger.info("system", `PlanGraph initialized: ${executionPlan.steps.length} nodes, ${this.planGraph.getState().parallelGroups.length} parallel groups`);
|
|
563
|
+
this.reflection.think("plan", `PlanGraph initialized with ${executionPlan.steps.length} nodes`);
|
|
564
|
+
exitPlan();
|
|
565
|
+
return executionPlan;
|
|
566
|
+
}
|
|
567
|
+
// Fallback: single-step plan
|
|
568
|
+
this._logger.logReasoning("Hierarchical planning disabled, using single-step fallback");
|
|
569
|
+
this.reflection.think("plan", "Using single-step fallback plan");
|
|
570
|
+
const fallbackPlan = {
|
|
571
|
+
goal,
|
|
572
|
+
steps: [
|
|
573
|
+
{
|
|
574
|
+
id: "step-1",
|
|
575
|
+
goal: `${approach.description}: ${goal}`,
|
|
576
|
+
targetFiles: [],
|
|
577
|
+
readFiles: [],
|
|
578
|
+
tools: ["file_read", "file_write", "file_edit", "grep", "glob"],
|
|
579
|
+
estimatedIterations: 10,
|
|
580
|
+
dependsOn: [],
|
|
581
|
+
},
|
|
582
|
+
],
|
|
583
|
+
estimatedTokens: 50_000,
|
|
584
|
+
};
|
|
585
|
+
// Initialize PlanGraphManager for fallback plan too
|
|
586
|
+
this.planGraph = PlanGraphManager.fromExecutionPlan(this.sessionId, fallbackPlan);
|
|
587
|
+
this._logger.info("system", `PlanGraph initialized (fallback): 1 node`);
|
|
588
|
+
exitPlan();
|
|
589
|
+
return fallbackPlan;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* 병렬 실행 가능 여부에 따라 DAG 기반 병렬 실행 또는 단일 step 실행을 선택한다.
|
|
593
|
+
*
|
|
594
|
+
* StateMachine의 `handleParallel` phase에서 호출될 때:
|
|
595
|
+
* - 첫 호출 시 DAG 기반 병렬 실행을 수행하고 모든 결과를 캐시한다.
|
|
596
|
+
* - 이후 호출은 캐시에서 결과를 반환한다.
|
|
597
|
+
*
|
|
598
|
+
* StateMachine의 `handleImplement` phase에서 호출될 때:
|
|
599
|
+
* - 기존 순차 실행을 그대로 사용한다.
|
|
600
|
+
*
|
|
601
|
+
* @param plan - 실행 계획
|
|
602
|
+
* @param stepIndex - 실행할 step 인덱스
|
|
603
|
+
* @param state - 현재 에이전트 상태
|
|
604
|
+
* @returns step 실행 결과
|
|
605
|
+
*/
|
|
606
|
+
async executeStepWithParallel(plan, stepIndex, state) {
|
|
607
|
+
// Parallel DAG execution: if enabled and PlanGraph exists and plan has multiple steps
|
|
608
|
+
const shouldRunParallel = this.config.enableParallelExecution &&
|
|
609
|
+
this.planGraph !== null &&
|
|
610
|
+
plan.steps.length > 1;
|
|
611
|
+
if (!shouldRunParallel) {
|
|
612
|
+
// Sequential fallback
|
|
613
|
+
return this.executeStep(plan, stepIndex, state);
|
|
614
|
+
}
|
|
615
|
+
// If DAG execution already ran, return cached result
|
|
616
|
+
if (this.parallelExecutionDone) {
|
|
617
|
+
const cached = this.parallelStepResults.get(stepIndex);
|
|
618
|
+
if (cached)
|
|
619
|
+
return cached;
|
|
620
|
+
// No cached result means this step was not reached by DAG (failed deps, etc.)
|
|
621
|
+
return {
|
|
622
|
+
stepIndex,
|
|
623
|
+
phase: "implement",
|
|
624
|
+
success: false,
|
|
625
|
+
output: `Step ${stepIndex} was not executed (blocked by dependency failure or skipped)`,
|
|
626
|
+
changedFiles: [],
|
|
627
|
+
tokensUsed: 0,
|
|
628
|
+
durationMs: 0,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
// First call triggers full DAG execution for ALL steps
|
|
632
|
+
await this.executeStepsWithDAG(plan, state);
|
|
633
|
+
this.parallelExecutionDone = true;
|
|
634
|
+
// Return this step's result from cache
|
|
635
|
+
const result = this.parallelStepResults.get(stepIndex);
|
|
636
|
+
if (result)
|
|
637
|
+
return result;
|
|
638
|
+
return {
|
|
639
|
+
stepIndex,
|
|
640
|
+
phase: "implement",
|
|
641
|
+
success: false,
|
|
642
|
+
output: `Step ${stepIndex} was not executed during DAG execution`,
|
|
643
|
+
changedFiles: [],
|
|
644
|
+
tokensUsed: 0,
|
|
645
|
+
durationMs: 0,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* PlanGraph DAG 기반 병렬 step 실행.
|
|
650
|
+
*
|
|
651
|
+
* PlanGraphManager의 의존성 그래프를 따라:
|
|
652
|
+
* 1. 의존성이 모두 완료된 ready 노드를 가져온다.
|
|
653
|
+
* 2. maxParallelAgents만큼 동시에 실행한다.
|
|
654
|
+
* 3. 완료되면 의존 노드를 ready로 전환하고 반복한다.
|
|
655
|
+
* 4. 모든 노드가 종료 상태가 될 때까지 반복한다.
|
|
656
|
+
*
|
|
657
|
+
* @param plan - 실행 계획
|
|
658
|
+
* @param state - 현재 에이전트 상태
|
|
659
|
+
*/
|
|
660
|
+
async executeStepsWithDAG(plan, state) {
|
|
661
|
+
if (!this.planGraph)
|
|
662
|
+
return;
|
|
663
|
+
const maxConcurrent = this.config.maxParallelAgents;
|
|
664
|
+
this._logger.info("system", `DAG parallel execution started: ${plan.steps.length} steps, max ${maxConcurrent} concurrent`);
|
|
665
|
+
this.reflection.think("implement", `Starting DAG-based parallel execution: ${plan.steps.length} steps, max ${maxConcurrent} concurrent`);
|
|
666
|
+
while (true) {
|
|
667
|
+
// Check abort
|
|
668
|
+
if (this.abortController.signal.aborted) {
|
|
669
|
+
this._logger.info("system", "DAG parallel execution aborted");
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
// Get nodes that are ready (all dependencies completed)
|
|
673
|
+
const readyNodes = this.planGraph.getReadyNodes();
|
|
674
|
+
if (readyNodes.length === 0) {
|
|
675
|
+
// Check if all done or stuck
|
|
676
|
+
if (this.planGraph.isComplete()) {
|
|
677
|
+
this._logger.info("system", "DAG parallel execution: all nodes complete");
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
// If there are running nodes, wait for them to finish
|
|
681
|
+
const graphState = this.planGraph.getState();
|
|
682
|
+
if (graphState.runningNodes.length > 0) {
|
|
683
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
// No ready, no running = deadlock (all remaining nodes are blocked/failed)
|
|
687
|
+
this._logger.warn("system", "DAG parallel execution: deadlock — no ready or running nodes, but graph not complete");
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
// Take up to maxConcurrent ready nodes
|
|
691
|
+
const batch = readyNodes.slice(0, maxConcurrent);
|
|
692
|
+
this._logger.info("system", `DAG batch: executing ${batch.length} steps in parallel [${batch.map(n => n.id).join(", ")}]`);
|
|
693
|
+
this.reflection.think("implement", `Parallel batch: ${batch.map(n => `"${n.goal}" (${n.id})`).join(", ")}`);
|
|
694
|
+
// Execute batch in parallel using Promise.allSettled
|
|
695
|
+
await Promise.allSettled(batch.map(async (node) => {
|
|
696
|
+
const stepIndex = plan.steps.findIndex(s => s.id === node.id);
|
|
697
|
+
if (stepIndex === -1) {
|
|
698
|
+
this._logger.warn("system", `DAG: node "${node.id}" not found in plan steps`);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
// executeStep already handles markRunning/markCompleted/markFailed
|
|
703
|
+
const stepResult = await this.executeStep(plan, stepIndex, state);
|
|
704
|
+
// Cache the result
|
|
705
|
+
this.parallelStepResults.set(stepIndex, stepResult);
|
|
706
|
+
// Update state tracking
|
|
707
|
+
state.currentStepIndex = Math.max(state.currentStepIndex, stepIndex + 1);
|
|
708
|
+
}
|
|
709
|
+
catch (err) {
|
|
710
|
+
// executeStep should not throw (it handles errors internally),
|
|
711
|
+
// but just in case, cache a failure result
|
|
712
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
713
|
+
this._logger.error("error", `DAG: unexpected error executing step ${stepIndex}: ${errorMessage}`);
|
|
714
|
+
this.parallelStepResults.set(stepIndex, {
|
|
715
|
+
stepIndex,
|
|
716
|
+
phase: "implement",
|
|
717
|
+
success: false,
|
|
718
|
+
output: `Unexpected error: ${errorMessage}`,
|
|
719
|
+
changedFiles: [],
|
|
720
|
+
tokensUsed: 0,
|
|
721
|
+
durationMs: 0,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
}));
|
|
725
|
+
// Checkpoint after each parallel batch
|
|
726
|
+
await this.checkpoint(state.currentStepIndex);
|
|
727
|
+
// Log progress
|
|
728
|
+
const progress = this.planGraph.getProgress();
|
|
729
|
+
this._logger.info("system", `DAG progress: ${progress.completed}/${progress.total} (${progress.percent}%)`);
|
|
730
|
+
}
|
|
731
|
+
// Final summary
|
|
732
|
+
const progress = this.planGraph.getProgress();
|
|
733
|
+
const graphState = this.planGraph.getState();
|
|
734
|
+
this._logger.info("system", `DAG parallel execution complete: ${progress.completed}/${progress.total} succeeded, ${graphState.failedNodes.length} failed, ${graphState.skippedNodes.length} skipped`);
|
|
735
|
+
this.reflection.think("implement", `DAG execution complete: ${progress.completed}/${progress.total} steps, ${graphState.failedNodes.length} failed`);
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* implement phase 콜백 — AgentLoop로 개별 step을 실행한다.
|
|
739
|
+
*
|
|
740
|
+
* @param plan - 실행 계획
|
|
741
|
+
* @param stepIndex - 실행할 step 인덱스
|
|
742
|
+
* @param state - 현재 에이전트 상태
|
|
743
|
+
* @returns step 실행 결과
|
|
744
|
+
*/
|
|
745
|
+
async executeStep(plan, stepIndex, state) {
|
|
746
|
+
const step = plan.steps[stepIndex];
|
|
747
|
+
if (!step) {
|
|
748
|
+
this._logger.warn("error", `Step ${stepIndex} not found in plan`);
|
|
749
|
+
return {
|
|
750
|
+
stepIndex,
|
|
751
|
+
phase: "implement",
|
|
752
|
+
success: false,
|
|
753
|
+
output: `Step ${stepIndex} not found in plan`,
|
|
754
|
+
changedFiles: [],
|
|
755
|
+
tokensUsed: 0,
|
|
756
|
+
durationMs: 0,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const exitImplement = this._logger.enterLayer("implement", `Step ${stepIndex + 1}/${plan.steps.length}: "${step.goal}"`);
|
|
760
|
+
this.reflection.think("implement", `Executing step ${stepIndex + 1}/${plan.steps.length}: "${step.goal}"`);
|
|
761
|
+
// Mark step as running in PlanGraph
|
|
762
|
+
const stepId = step.id;
|
|
763
|
+
if (this.planGraph) {
|
|
764
|
+
try {
|
|
765
|
+
this.planGraph.markRunning(stepId);
|
|
766
|
+
this._logger.info("system", `PlanGraph: node "${stepId}" → running`);
|
|
767
|
+
}
|
|
768
|
+
catch (pgErr) {
|
|
769
|
+
// Node may not be in ready state (e.g., if called out of order)
|
|
770
|
+
this._logger.warn("system", `PlanGraph: failed to mark "${stepId}" running: ${pgErr instanceof Error ? pgErr.message : String(pgErr)}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
// Snapshot target files before modification
|
|
774
|
+
await this.snapshotFiles(step.targetFiles);
|
|
775
|
+
// ─── Speculative Execution (opt-in) ─────────────────────────
|
|
776
|
+
if (this.config.enableSpeculative) {
|
|
777
|
+
try {
|
|
778
|
+
this._logger.info("system", `Speculative execution enabled for step ${stepIndex + 1}`);
|
|
779
|
+
this.reflection.think("implement", `Using speculative execution for step ${stepIndex + 1}`);
|
|
780
|
+
const specExecutor = new SpeculativeExecutor({
|
|
781
|
+
maxApproaches: this.config.speculativeMaxApproaches,
|
|
782
|
+
approachTimeout: 120_000,
|
|
783
|
+
minQualityThreshold: 60,
|
|
784
|
+
byokConfig: this.config.byokConfig,
|
|
785
|
+
projectPath: this.config.projectPath,
|
|
786
|
+
});
|
|
787
|
+
// Forward speculative events
|
|
788
|
+
const specEvents = [
|
|
789
|
+
"speculative:start",
|
|
790
|
+
"speculative:approach:start",
|
|
791
|
+
"speculative:approach:complete",
|
|
792
|
+
"speculative:evaluation",
|
|
793
|
+
"speculative:complete",
|
|
794
|
+
];
|
|
795
|
+
for (const eventName of specEvents) {
|
|
796
|
+
specExecutor.on(eventName, (...args) => {
|
|
797
|
+
this.emit(eventName, ...args);
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
// Build codebase context summary for speculative executor
|
|
801
|
+
let codebaseContextSummary = "";
|
|
802
|
+
if (this.codebaseContext) {
|
|
803
|
+
const stats = this.codebaseContext.getStats();
|
|
804
|
+
codebaseContextSummary = `Project: ${stats.totalFiles} files, ${stats.totalSymbols} symbols. Path: ${this.config.projectPath}`;
|
|
805
|
+
}
|
|
806
|
+
const specResult = await specExecutor.execute(step.goal, this.config.toolExecutor, codebaseContextSummary);
|
|
807
|
+
if (specResult.winner) {
|
|
808
|
+
this._logger.info("system", `Speculative execution winner: "${specResult.winner.approach.strategy}" (quality: ${specResult.winner.qualityScore}, tokens: ${specResult.winner.tokensUsed})`);
|
|
809
|
+
this.reflection.think("implement", `Speculative winner: ${specResult.winner.approach.strategy} — ${specResult.selectionReason}`);
|
|
810
|
+
// Apply winner's file changes
|
|
811
|
+
for (const [filePath] of specResult.winner.changes) {
|
|
812
|
+
this.changedFiles.add(filePath);
|
|
813
|
+
}
|
|
814
|
+
// Update PlanGraph
|
|
815
|
+
if (this.planGraph) {
|
|
816
|
+
try {
|
|
817
|
+
this.planGraph.markCompleted(stepId, `Speculative execution (${specResult.winner.approach.strategy}): quality ${specResult.winner.qualityScore}`, [...specResult.winner.changes.keys()], { input: specResult.totalTokensUsed, output: 0 });
|
|
818
|
+
this._logger.info("system", `PlanGraph: node "${stepId}" → completed (speculative)`);
|
|
819
|
+
}
|
|
820
|
+
catch (pgErr) {
|
|
821
|
+
this._logger.warn("system", `PlanGraph: failed to update "${stepId}": ${pgErr instanceof Error ? pgErr.message : String(pgErr)}`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
// Checkpoint and return
|
|
825
|
+
await this.checkpoint(stepIndex);
|
|
826
|
+
exitImplement();
|
|
827
|
+
return {
|
|
828
|
+
stepIndex,
|
|
829
|
+
phase: "implement",
|
|
830
|
+
success: specResult.winner.success,
|
|
831
|
+
output: `Speculative execution (${specResult.winner.approach.strategy}): ${specResult.selectionReason}`,
|
|
832
|
+
changedFiles: [...specResult.winner.changes.keys()],
|
|
833
|
+
tokensUsed: specResult.totalTokensUsed,
|
|
834
|
+
durationMs: specResult.winner.durationMs,
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
// No winner — fall through to normal AgentLoop execution
|
|
838
|
+
this._logger.info("system", "Speculative execution: no winner found, falling back to normal execution");
|
|
839
|
+
this.reflection.think("implement", "Speculative execution produced no winner — falling back to AgentLoop");
|
|
840
|
+
if (specResult.learnings.length > 0) {
|
|
841
|
+
this.reflection.think("implement", `Speculative learnings: ${specResult.learnings.join("; ")}`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
catch (specErr) {
|
|
845
|
+
this._logger.warn("system", `Speculative execution failed (falling back to normal): ${specErr instanceof Error ? specErr.message : String(specErr)}`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
// ─── Normal AgentLoop Execution ─────────────────────────────
|
|
849
|
+
// Build step-specific system prompt
|
|
850
|
+
const systemPrompt = this.buildStepPrompt(plan, step, stepIndex);
|
|
851
|
+
// Create and run AgentLoop for this step
|
|
852
|
+
const loop = this.createAgentLoop(systemPrompt);
|
|
853
|
+
this.wireAgentLoopEvents(loop);
|
|
854
|
+
const startMs = Date.now();
|
|
855
|
+
const termination = await loop.run(step.goal);
|
|
856
|
+
const durationMs = Date.now() - startMs;
|
|
857
|
+
// Track last termination
|
|
858
|
+
this.lastTermination = termination;
|
|
859
|
+
// Collect changed files from loop
|
|
860
|
+
const loopUsage = loop.getTokenUsage();
|
|
861
|
+
const stepChangedFiles = [];
|
|
862
|
+
// Extract changed files from agent events (tracked via tool results)
|
|
863
|
+
// The changedFiles set is updated via wireAgentLoopEvents
|
|
864
|
+
for (const file of this.changedFiles) {
|
|
865
|
+
if (step.targetFiles.includes(file) || step.targetFiles.length === 0) {
|
|
866
|
+
stepChangedFiles.push(file);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const success = termination.reason === "GOAL_ACHIEVED";
|
|
870
|
+
// Update PlanGraph with step outcome
|
|
871
|
+
if (this.planGraph) {
|
|
872
|
+
try {
|
|
873
|
+
if (success) {
|
|
874
|
+
this.planGraph.markCompleted(stepId, termination.reason === "GOAL_ACHIEVED"
|
|
875
|
+
? termination.summary
|
|
876
|
+
: "completed", stepChangedFiles, { input: loopUsage.input, output: loopUsage.output });
|
|
877
|
+
this._logger.info("system", `PlanGraph: node "${stepId}" → completed`);
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
const errorMsg = termination.reason === "ERROR"
|
|
881
|
+
? termination.error
|
|
882
|
+
: `Step failed: ${termination.reason}`;
|
|
883
|
+
this.planGraph.markFailed(stepId, errorMsg);
|
|
884
|
+
this._logger.info("system", `PlanGraph: node "${stepId}" → failed`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
catch (pgErr) {
|
|
888
|
+
this._logger.warn("system", `PlanGraph: failed to update "${stepId}": ${pgErr instanceof Error ? pgErr.message : String(pgErr)}`);
|
|
889
|
+
}
|
|
890
|
+
// Log plan progress after each step
|
|
891
|
+
const progress = this.planGraph.getProgress();
|
|
892
|
+
this._logger.info("system", `PlanGraph progress: ${progress.completed}/${progress.total} (${progress.percent}%)`);
|
|
893
|
+
}
|
|
894
|
+
this._logger.logDecision(`Step ${stepIndex + 1} outcome`, ["succeeded", "failed"], success ? "succeeded" : "failed", `AgentLoop terminated with reason: ${termination.reason}`);
|
|
895
|
+
this.reflection.think("implement", `Step ${stepIndex + 1} ${success ? "succeeded" : "failed"}: ${termination.reason === "GOAL_ACHIEVED"
|
|
896
|
+
? termination.summary
|
|
897
|
+
: termination.reason}`);
|
|
898
|
+
// Checkpoint after step completes (success or failure)
|
|
899
|
+
await this.checkpoint(stepIndex);
|
|
900
|
+
exitImplement();
|
|
901
|
+
return {
|
|
902
|
+
stepIndex,
|
|
903
|
+
phase: "implement",
|
|
904
|
+
success,
|
|
905
|
+
output: termination.reason === "GOAL_ACHIEVED"
|
|
906
|
+
? termination.summary
|
|
907
|
+
: termination.reason === "ERROR"
|
|
908
|
+
? termination.error
|
|
909
|
+
: `Terminated: ${termination.reason}`,
|
|
910
|
+
changedFiles: stepChangedFiles,
|
|
911
|
+
tokensUsed: loopUsage.input + loopUsage.output,
|
|
912
|
+
durationMs,
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* verify phase 콜백 — SelfReflection.deepVerify로 6-dimension 검증을 수행한다.
|
|
917
|
+
*
|
|
918
|
+
* @param state - 현재 에이전트 상태
|
|
919
|
+
* @returns 검증 결과 (StateMachine VerifyResult 형식)
|
|
920
|
+
*/
|
|
921
|
+
async verifyWork(state) {
|
|
922
|
+
const exitVerify = this._logger.enterLayer("verify", "Starting deep verification");
|
|
923
|
+
this.reflection.think("verify", "Starting deep verification...");
|
|
924
|
+
if (!this.config.enableDeepVerify || this.changedFiles.size === 0) {
|
|
925
|
+
this._logger.logReasoning("Deep verify skipped", ["run deep verify", "skip deep verify"], "skip deep verify", undefined, "Deep verify disabled or no changed files");
|
|
926
|
+
this.reflection.think("verify", "Deep verify skipped (disabled or no changed files)");
|
|
927
|
+
exitVerify();
|
|
928
|
+
return {
|
|
929
|
+
verdict: "pass",
|
|
930
|
+
checks: {
|
|
931
|
+
buildSuccess: true,
|
|
932
|
+
typesSafe: true,
|
|
933
|
+
testsPass: true,
|
|
934
|
+
noRegressions: true,
|
|
935
|
+
followsPatterns: true,
|
|
936
|
+
securityClean: true,
|
|
937
|
+
},
|
|
938
|
+
issues: [],
|
|
939
|
+
suggestions: [],
|
|
940
|
+
confidence: 1.0,
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
// Collect current file contents
|
|
944
|
+
const changedFileContents = new Map();
|
|
945
|
+
for (const filePath of this.changedFiles) {
|
|
946
|
+
try {
|
|
947
|
+
const content = await readFile(filePath, "utf-8");
|
|
948
|
+
changedFileContents.set(filePath, content);
|
|
949
|
+
}
|
|
950
|
+
catch {
|
|
951
|
+
// File may have been deleted
|
|
952
|
+
changedFileContents.set(filePath, "");
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
// Gather project patterns from codebase context
|
|
956
|
+
const patterns = [];
|
|
957
|
+
if (this.codebaseContext) {
|
|
958
|
+
const stats = this.codebaseContext.getStats();
|
|
959
|
+
if (stats.totalFiles > 0) {
|
|
960
|
+
patterns.push(`Project has ${stats.totalFiles} files`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
// Run deep verification
|
|
964
|
+
const deepResult = await this.reflection.deepVerify(state.goal, changedFileContents, this.originalFiles, patterns, async (prompt) => {
|
|
965
|
+
const response = await this.llmClient.chat([
|
|
966
|
+
{ role: "user", content: prompt },
|
|
967
|
+
]);
|
|
968
|
+
return response.content ?? "";
|
|
969
|
+
});
|
|
970
|
+
this.lastVerifyResult = deepResult;
|
|
971
|
+
// Cross-check with PlanGraph completion status
|
|
972
|
+
if (this.planGraph) {
|
|
973
|
+
const pgComplete = this.planGraph.isComplete();
|
|
974
|
+
const progress = this.planGraph.getProgress();
|
|
975
|
+
this._logger.info("system", `PlanGraph completion check: complete=${pgComplete}, ${progress.completed}/${progress.total} nodes`);
|
|
976
|
+
if (!pgComplete && deepResult.verdict === "pass") {
|
|
977
|
+
this.reflection.think("verify", `Warning: Deep verify passed but PlanGraph shows ${progress.total - progress.completed} incomplete nodes`);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
this._logger.logDecision("Verification verdict", ["pass", "warn", "fail"], deepResult.verdict, `Score: ${deepResult.overallScore}, confidence: ${deepResult.confidence}`);
|
|
981
|
+
this.reflection.think("verify", `Deep verification: ${deepResult.verdict} (score: ${deepResult.overallScore}, confidence: ${deepResult.confidence})`);
|
|
982
|
+
// ─── Debate Orchestrator (optional) ───────────────────────
|
|
983
|
+
if (this.config.enableDebate && this.changedFiles.size > 0) {
|
|
984
|
+
try {
|
|
985
|
+
this._logger.info("system", "Running multi-agent debate verification...");
|
|
986
|
+
this.reflection.think("verify", "Starting debate orchestrator for code review...");
|
|
987
|
+
const debateOrchestrator = DebateOrchestrator.create({
|
|
988
|
+
projectPath: this.config.projectPath,
|
|
989
|
+
byokConfig: this.config.byokConfig,
|
|
990
|
+
toolExecutor: this.config.toolExecutor,
|
|
991
|
+
});
|
|
992
|
+
// Forward debate events
|
|
993
|
+
const debateEvents = [
|
|
994
|
+
"debate:start",
|
|
995
|
+
"debate:round:start",
|
|
996
|
+
"debate:round:end",
|
|
997
|
+
"debate:coder",
|
|
998
|
+
"debate:reviewer",
|
|
999
|
+
"debate:revision",
|
|
1000
|
+
"debate:verifier",
|
|
1001
|
+
"debate:pass",
|
|
1002
|
+
"debate:fail",
|
|
1003
|
+
"debate:token_usage",
|
|
1004
|
+
"debate:abort",
|
|
1005
|
+
];
|
|
1006
|
+
for (const eventName of debateEvents) {
|
|
1007
|
+
debateOrchestrator.on(eventName, (...args) => {
|
|
1008
|
+
this.emit(eventName, ...args);
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
const changedFilesSummary = [...this.changedFiles]
|
|
1012
|
+
.map((f) => {
|
|
1013
|
+
const content = changedFileContents.get(f) ?? "";
|
|
1014
|
+
return `## ${f}\n${content.slice(0, 2000)}`;
|
|
1015
|
+
})
|
|
1016
|
+
.join("\n\n");
|
|
1017
|
+
const debateResult = await debateOrchestrator.debate(state.goal, changedFilesSummary);
|
|
1018
|
+
this._logger.info("system", `Debate result: score=${debateResult.finalScore}, success=${debateResult.success}, rounds=${debateResult.rounds.length}`);
|
|
1019
|
+
this.reflection.think("verify", `Debate: ${debateResult.success ? "PASSED" : "FAILED"} (score: ${debateResult.finalScore}, ${debateResult.rounds.length} round(s))`);
|
|
1020
|
+
// If debate fails and deep verify passed, downgrade to "concern"
|
|
1021
|
+
if (!debateResult.success && deepResult.verdict === "pass") {
|
|
1022
|
+
deepResult.verdict = "concern";
|
|
1023
|
+
const reviewerIssues = debateResult.rounds
|
|
1024
|
+
.flatMap((r) => r.issues)
|
|
1025
|
+
.filter((i) => i.severity === "critical" || i.severity === "major");
|
|
1026
|
+
deepResult.suggestedFixes.push(...reviewerIssues.map((i) => ({
|
|
1027
|
+
dimension: "correctness",
|
|
1028
|
+
severity: i.severity === "critical" ? "critical" : "high",
|
|
1029
|
+
description: `[debate] ${i.description}`,
|
|
1030
|
+
file: i.file,
|
|
1031
|
+
autoFixable: false,
|
|
1032
|
+
})));
|
|
1033
|
+
this.reflection.think("verify", `Debate downgraded verdict to "concern" due to ${reviewerIssues.length} critical/major issue(s)`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
catch (debateErr) {
|
|
1037
|
+
this._logger.warn("system", `Debate orchestrator failed (continuing): ${debateErr instanceof Error ? debateErr.message : String(debateErr)}`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
// ─── Security Scanner (optional) ──────────────────────────
|
|
1041
|
+
if (this.config.enableSecurityScan && this.changedFiles.size > 0) {
|
|
1042
|
+
try {
|
|
1043
|
+
this._logger.info("system", "Running security scan on changed files...");
|
|
1044
|
+
this.reflection.think("verify", "Running security scanner...");
|
|
1045
|
+
const scanner = new SecurityScanner({
|
|
1046
|
+
projectPath: this.config.projectPath,
|
|
1047
|
+
});
|
|
1048
|
+
const changedFileList = [...this.changedFiles];
|
|
1049
|
+
const scanResult = await scanner.scan(changedFileList);
|
|
1050
|
+
this._logger.info("system", `Security scan: passed=${scanResult.passed}, findings=${scanResult.summary.total} (critical=${scanResult.summary.critical}, high=${scanResult.summary.high})`);
|
|
1051
|
+
this.reflection.think("verify", `Security scan: ${scanResult.passed ? "PASSED" : "FAILED"} — ${scanResult.summary.total} finding(s) (${scanResult.summary.critical} critical, ${scanResult.summary.high} high)`);
|
|
1052
|
+
// If critical findings found, add them to suggestedFixes and downgrade verdict
|
|
1053
|
+
if (scanResult.summary.critical > 0 || scanResult.summary.high > 0) {
|
|
1054
|
+
const securityFindings = scanResult.findings
|
|
1055
|
+
.filter((f) => f.severity === "critical" || f.severity === "high");
|
|
1056
|
+
deepResult.suggestedFixes.push(...securityFindings.map((f) => ({
|
|
1057
|
+
dimension: "security",
|
|
1058
|
+
severity: f.severity === "critical" ? "critical" : "high",
|
|
1059
|
+
description: `[security] ${f.rule} in ${f.file}:${f.line} — ${f.message}`,
|
|
1060
|
+
file: f.file,
|
|
1061
|
+
autoFixable: false,
|
|
1062
|
+
})));
|
|
1063
|
+
if (scanResult.summary.critical > 0 && deepResult.verdict !== "fail") {
|
|
1064
|
+
deepResult.verdict = "fail";
|
|
1065
|
+
this.reflection.think("verify", `Security scanner escalated verdict to "fail" due to ${scanResult.summary.critical} critical finding(s)`);
|
|
1066
|
+
}
|
|
1067
|
+
else if (deepResult.verdict === "pass") {
|
|
1068
|
+
deepResult.verdict = "concern";
|
|
1069
|
+
this.reflection.think("verify", `Security scanner downgraded verdict to "concern" due to high-severity findings`);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
catch (scanErr) {
|
|
1074
|
+
this._logger.warn("system", `Security scan failed (continuing): ${scanErr instanceof Error ? scanErr.message : String(scanErr)}`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
// ─── Doc Intelligence (optional) ──────────────────────────
|
|
1078
|
+
if (this.config.enableDocGeneration && this.changedFiles.size > 0) {
|
|
1079
|
+
try {
|
|
1080
|
+
this._logger.info("system", "Running documentation intelligence...");
|
|
1081
|
+
this.reflection.think("verify", "Analyzing documentation coverage...");
|
|
1082
|
+
const docIntel = new DocIntelligence({
|
|
1083
|
+
projectPath: this.config.projectPath,
|
|
1084
|
+
});
|
|
1085
|
+
const docCoverage = docIntel.analyzeCoverage(changedFileContents);
|
|
1086
|
+
this._logger.info("system", `Doc coverage: ${docCoverage.coveragePercent}% (grade: ${docCoverage.grade}), ${docCoverage.missing.length} undocumented symbol(s)`);
|
|
1087
|
+
this.reflection.think("verify", `Documentation: ${docCoverage.coveragePercent}% coverage (grade ${docCoverage.grade}), ${docCoverage.missing.length} missing`);
|
|
1088
|
+
// If coverage is below D grade, add as suggestedFix (non-blocking)
|
|
1089
|
+
if (docCoverage.grade === "D" || docCoverage.grade === "F") {
|
|
1090
|
+
const missingSymbols = docCoverage.missing
|
|
1091
|
+
.slice(0, 10)
|
|
1092
|
+
.map((m) => `${m.symbolName} (${m.symbolType}) in ${m.filePath}:${m.line}`);
|
|
1093
|
+
deepResult.suggestedFixes.push({
|
|
1094
|
+
dimension: "quality",
|
|
1095
|
+
severity: "low",
|
|
1096
|
+
description: `Documentation coverage is ${docCoverage.grade} (${docCoverage.coveragePercent}%). Missing JSDoc for: ${missingSymbols.join(", ")}`,
|
|
1097
|
+
autoFixable: false,
|
|
1098
|
+
});
|
|
1099
|
+
this.reflection.think("verify", `Low doc coverage (${docCoverage.grade}) — added suggestion for ${missingSymbols.length} symbol(s)`);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
catch (docErr) {
|
|
1103
|
+
this._logger.warn("system", `Doc intelligence failed (continuing): ${docErr instanceof Error ? docErr.message : String(docErr)}`);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
exitVerify();
|
|
1107
|
+
// Map DeepVerifyResult → VerifyResult
|
|
1108
|
+
return this.mapVerifyResult(deepResult);
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* fix phase 콜백 — AgentLoop로 에러를 수정한다.
|
|
1112
|
+
*
|
|
1113
|
+
* @param errors - 수정할 에러 목록
|
|
1114
|
+
* @param state - 현재 에이전트 상태
|
|
1115
|
+
* @returns 수정 시도 결과
|
|
1116
|
+
*/
|
|
1117
|
+
async fixErrors(errors, state) {
|
|
1118
|
+
const exitFix = this._logger.enterLayer("fix", `Fixing ${errors.length} error(s)`);
|
|
1119
|
+
const errorDescriptions = errors
|
|
1120
|
+
.map((e, i) => `${i + 1}. [${e.phase}] ${e.error}${e.suggestedFix ? ` (suggested: ${e.suggestedFix})` : ""}`)
|
|
1121
|
+
.join("\n");
|
|
1122
|
+
this._logger.info("error", `Errors to fix: ${errors.length}`, {
|
|
1123
|
+
errors: errors.map((e) => ({ phase: e.phase, error: e.error })),
|
|
1124
|
+
});
|
|
1125
|
+
this.reflection.think("fix", `Attempting to fix ${errors.length} error(s):\n${errorDescriptions}`);
|
|
1126
|
+
const fixPrompt = `You are fixing errors found during verification. Address each issue:
|
|
1127
|
+
|
|
1128
|
+
## Errors to Fix
|
|
1129
|
+
${errorDescriptions}
|
|
1130
|
+
|
|
1131
|
+
## Changed Files
|
|
1132
|
+
${[...this.changedFiles].join(", ")}
|
|
1133
|
+
|
|
1134
|
+
## Instructions
|
|
1135
|
+
- Fix each error by reading the affected file and making corrections
|
|
1136
|
+
- Run relevant build/lint checks after each fix
|
|
1137
|
+
- Be precise — only change what's needed to fix the issue
|
|
1138
|
+
- If an error seems unfixable, explain why`;
|
|
1139
|
+
const loop = this.createAgentLoop(fixPrompt);
|
|
1140
|
+
this.wireAgentLoopEvents(loop);
|
|
1141
|
+
const startMs = Date.now();
|
|
1142
|
+
const termination = await loop.run(`Fix the following errors:\n${errorDescriptions}`);
|
|
1143
|
+
const durationMs = Date.now() - startMs;
|
|
1144
|
+
this.lastTermination = termination;
|
|
1145
|
+
const loopUsage = loop.getTokenUsage();
|
|
1146
|
+
const success = termination.reason === "GOAL_ACHIEVED";
|
|
1147
|
+
this._logger.logDecision("Fix attempt result", ["succeeded", "failed"], success ? "succeeded" : "failed", `Fix loop terminated with reason: ${termination.reason}`);
|
|
1148
|
+
this.reflection.think("fix", `Fix attempt ${success ? "succeeded" : "failed"}: ${termination.reason}`);
|
|
1149
|
+
exitFix();
|
|
1150
|
+
return {
|
|
1151
|
+
stepIndex: state.currentStepIndex,
|
|
1152
|
+
phase: "fix",
|
|
1153
|
+
success,
|
|
1154
|
+
output: termination.reason === "GOAL_ACHIEVED"
|
|
1155
|
+
? termination.summary
|
|
1156
|
+
: `Fix attempt ended: ${termination.reason}`,
|
|
1157
|
+
changedFiles: [...this.changedFiles],
|
|
1158
|
+
tokensUsed: loopUsage.input + loopUsage.output,
|
|
1159
|
+
durationMs,
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* replan phase 콜백 — HierarchicalPlanner.replan으로 재계획을 수립한다.
|
|
1164
|
+
*
|
|
1165
|
+
* @param state - 현재 에이전트 상태
|
|
1166
|
+
* @returns 새로운 실행 계획
|
|
1167
|
+
*/
|
|
1168
|
+
async replanWork(state) {
|
|
1169
|
+
const exitReplan = this._logger.enterLayer("replan", `Re-planning due to ${state.errors.length} error(s)`);
|
|
1170
|
+
this.reflection.think("replan", `Re-planning due to ${state.errors.length} error(s)`);
|
|
1171
|
+
if (this.config.enableHierarchicalPlanning && this.hierarchicalPlan) {
|
|
1172
|
+
const planner = new HierarchicalPlanner({
|
|
1173
|
+
projectPath: this.config.projectPath,
|
|
1174
|
+
});
|
|
1175
|
+
const errorDescriptions = state.errors
|
|
1176
|
+
.map((e) => e.error)
|
|
1177
|
+
.join("; ");
|
|
1178
|
+
const affectedTaskIds = state.errors
|
|
1179
|
+
.map((e) => state.plan?.steps[e.stepIndex]?.id)
|
|
1180
|
+
.filter((id) => !!id);
|
|
1181
|
+
const replanResult = await planner.replan(this.hierarchicalPlan, {
|
|
1182
|
+
type: "error",
|
|
1183
|
+
description: errorDescriptions,
|
|
1184
|
+
affectedTaskIds,
|
|
1185
|
+
severity: "major",
|
|
1186
|
+
}, this.llmClient);
|
|
1187
|
+
this._logger.logDecision("Replan strategy", ["retry", "simplify", "restructure", "abort"], replanResult.strategy, replanResult.reason);
|
|
1188
|
+
this.reflection.think("replan", `Re-plan strategy: ${replanResult.strategy} — ${replanResult.reason}`);
|
|
1189
|
+
// Convert modified tasks back to ExecutionPlan
|
|
1190
|
+
const newSteps = replanResult.modifiedTasks.map((task) => ({
|
|
1191
|
+
id: task.id,
|
|
1192
|
+
goal: task.description,
|
|
1193
|
+
targetFiles: task.targetFiles,
|
|
1194
|
+
readFiles: task.readFiles,
|
|
1195
|
+
tools: task.toolStrategy,
|
|
1196
|
+
estimatedIterations: task.estimatedIterations,
|
|
1197
|
+
dependsOn: task.dependsOn,
|
|
1198
|
+
}));
|
|
1199
|
+
const newPlan = {
|
|
1200
|
+
goal: state.goal,
|
|
1201
|
+
steps: newSteps,
|
|
1202
|
+
estimatedTokens: newSteps.length * 10_000,
|
|
1203
|
+
};
|
|
1204
|
+
// Re-initialize PlanGraph for the new plan
|
|
1205
|
+
this.planGraph = PlanGraphManager.fromExecutionPlan(this.sessionId, newPlan);
|
|
1206
|
+
this._logger.info("system", `PlanGraph re-initialized (replan): ${newSteps.length} nodes`);
|
|
1207
|
+
// Checkpoint after replan completes (save new plan state)
|
|
1208
|
+
await this.checkpoint(state.currentStepIndex);
|
|
1209
|
+
exitReplan();
|
|
1210
|
+
return newPlan;
|
|
1211
|
+
}
|
|
1212
|
+
// Fallback: single retry step
|
|
1213
|
+
this._logger.logReasoning("Hierarchical replanning unavailable, using single-step fallback");
|
|
1214
|
+
this.reflection.think("replan", "Using fallback single-step replan");
|
|
1215
|
+
const fallbackReplan = {
|
|
1216
|
+
goal: state.goal,
|
|
1217
|
+
steps: [
|
|
1218
|
+
{
|
|
1219
|
+
id: "replan-step-1",
|
|
1220
|
+
goal: `Retry: ${state.goal} (previous errors: ${state.errors.map((e) => e.error).join("; ")})`,
|
|
1221
|
+
targetFiles: [],
|
|
1222
|
+
readFiles: [],
|
|
1223
|
+
tools: ["file_read", "file_write", "file_edit", "grep", "glob"],
|
|
1224
|
+
estimatedIterations: 10,
|
|
1225
|
+
dependsOn: [],
|
|
1226
|
+
},
|
|
1227
|
+
],
|
|
1228
|
+
estimatedTokens: 50_000,
|
|
1229
|
+
};
|
|
1230
|
+
// Re-initialize PlanGraph for fallback replan
|
|
1231
|
+
this.planGraph = PlanGraphManager.fromExecutionPlan(this.sessionId, fallbackReplan);
|
|
1232
|
+
this._logger.info("system", `PlanGraph re-initialized (fallback replan): 1 node`);
|
|
1233
|
+
// Checkpoint after fallback replan completes
|
|
1234
|
+
await this.checkpoint(state.currentStepIndex);
|
|
1235
|
+
exitReplan();
|
|
1236
|
+
return fallbackReplan;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* delegate phase 콜백 — 사용자에게 질문을 전달하고 응답을 대기한다.
|
|
1240
|
+
*
|
|
1241
|
+
* 현재 구현에서는 기본 응답을 반환한다.
|
|
1242
|
+
* 실제 환경에서는 이벤트를 emit하고 외부 응답을 대기해야 한다.
|
|
1243
|
+
*
|
|
1244
|
+
* @param question - 사용자에게 전달할 질문
|
|
1245
|
+
* @returns 사용자 응답
|
|
1246
|
+
*/
|
|
1247
|
+
async delegateToUser(question) {
|
|
1248
|
+
this.reflection.think("delegate", `Delegating to user: "${question}"`);
|
|
1249
|
+
// Emit event for external handling
|
|
1250
|
+
this.emit("engine:delegate", question);
|
|
1251
|
+
// In a real implementation, this would wait for user input.
|
|
1252
|
+
// For now, return a default that selects the recommended approach.
|
|
1253
|
+
return "1";
|
|
1254
|
+
}
|
|
1255
|
+
// ─── Helpers ───────────────────────────────────────────────────
|
|
1256
|
+
/**
|
|
1257
|
+
* 변경 전 파일 내용을 스냅샷으로 저장한다.
|
|
1258
|
+
* 이미 스냅샷이 있는 파일은 건너뛴다 (최초 원본만 보존).
|
|
1259
|
+
*
|
|
1260
|
+
* @param files - 스냅샷할 파일 경로 목록
|
|
1261
|
+
*/
|
|
1262
|
+
async snapshotFiles(files) {
|
|
1263
|
+
for (const file of files) {
|
|
1264
|
+
if (this.originalFiles.has(file))
|
|
1265
|
+
continue;
|
|
1266
|
+
try {
|
|
1267
|
+
const content = await readFile(file, "utf-8");
|
|
1268
|
+
this.originalFiles.set(file, content);
|
|
1269
|
+
}
|
|
1270
|
+
catch {
|
|
1271
|
+
// File doesn't exist yet — no snapshot needed
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* 개별 step 실행을 위한 AgentLoop 인스턴스를 생성한다.
|
|
1277
|
+
*
|
|
1278
|
+
* @param systemPrompt - step별 시스템 프롬프트
|
|
1279
|
+
* @returns 새 AgentLoop 인스턴스
|
|
1280
|
+
*/
|
|
1281
|
+
createAgentLoop(systemPrompt) {
|
|
1282
|
+
// Merge base tool definitions with MCP tool definitions
|
|
1283
|
+
const tools = [
|
|
1284
|
+
...this.config.toolExecutor.definitions,
|
|
1285
|
+
...this.mcpToolDefinitions,
|
|
1286
|
+
];
|
|
1287
|
+
// Wrap tool executor to add sandbox validation and MCP tool routing
|
|
1288
|
+
const baseExecutor = this.config.toolExecutor;
|
|
1289
|
+
const sandboxMgr = this.sandboxManager;
|
|
1290
|
+
const mcpCli = this.mcpClient;
|
|
1291
|
+
const mcpToolDefs = this.mcpToolDefinitions;
|
|
1292
|
+
const wrappedExecutor = {
|
|
1293
|
+
definitions: tools,
|
|
1294
|
+
execute: async (call, abortSignal) => {
|
|
1295
|
+
// Parse arguments to object if needed
|
|
1296
|
+
let args;
|
|
1297
|
+
try {
|
|
1298
|
+
args = typeof call.arguments === "string"
|
|
1299
|
+
? JSON.parse(call.arguments)
|
|
1300
|
+
: call.arguments;
|
|
1301
|
+
}
|
|
1302
|
+
catch {
|
|
1303
|
+
return {
|
|
1304
|
+
tool_call_id: call.id,
|
|
1305
|
+
name: call.name,
|
|
1306
|
+
output: `[Error] Invalid JSON in tool arguments: ${String(call.arguments).slice(0, 200)}`,
|
|
1307
|
+
success: false,
|
|
1308
|
+
durationMs: 0,
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
// Sandbox pre-validation
|
|
1312
|
+
if (sandboxMgr) {
|
|
1313
|
+
const validation = sandboxMgr.validateToolCall(call.name, args);
|
|
1314
|
+
if (!validation.allowed) {
|
|
1315
|
+
return {
|
|
1316
|
+
tool_call_id: call.id,
|
|
1317
|
+
name: call.name,
|
|
1318
|
+
output: `[Sandbox blocked] ${validation.violations.join("; ")}`,
|
|
1319
|
+
success: false,
|
|
1320
|
+
durationMs: 0,
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
// Route MCP tool calls to MCPClient (check MCP registry, not string pattern)
|
|
1325
|
+
if (mcpCli && mcpToolDefs.some((t) => t.name === call.name)) {
|
|
1326
|
+
return mcpCli.callToolAsYuan(call.name, args, call.id);
|
|
1327
|
+
}
|
|
1328
|
+
// Default: use base executor
|
|
1329
|
+
return baseExecutor.execute(call, abortSignal);
|
|
1330
|
+
},
|
|
1331
|
+
};
|
|
1332
|
+
return new AgentLoop({
|
|
1333
|
+
config: {
|
|
1334
|
+
byok: this.config.byokConfig,
|
|
1335
|
+
loop: {
|
|
1336
|
+
model: "coding",
|
|
1337
|
+
maxIterations: this.config.maxIterations,
|
|
1338
|
+
maxTokensPerIteration: 16_384,
|
|
1339
|
+
totalTokenBudget: this.config.totalTokenBudget,
|
|
1340
|
+
tools,
|
|
1341
|
+
systemPrompt,
|
|
1342
|
+
projectPath: this.config.projectPath,
|
|
1343
|
+
},
|
|
1344
|
+
},
|
|
1345
|
+
toolExecutor: wrappedExecutor,
|
|
1346
|
+
governorConfig: this.config.governorConfig ?? { planTier: "PRO" },
|
|
1347
|
+
contextConfig: this.config.contextConfig,
|
|
1348
|
+
approvalConfig: this.config.approvalConfig,
|
|
1349
|
+
approvalHandler: this.config.approvalHandler,
|
|
1350
|
+
autoFixConfig: this.config.autoFixConfig,
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* AgentLoop 이벤트를 ExecutionEngine 이벤트로 전파한다.
|
|
1355
|
+
*
|
|
1356
|
+
* @param loop - 이벤트를 연결할 AgentLoop 인스턴스
|
|
1357
|
+
*/
|
|
1358
|
+
wireAgentLoopEvents(loop) {
|
|
1359
|
+
loop.on("event", (event) => {
|
|
1360
|
+
switch (event.kind) {
|
|
1361
|
+
case "agent:text_delta":
|
|
1362
|
+
this.emit("text_delta", event.text);
|
|
1363
|
+
break;
|
|
1364
|
+
case "agent:thinking":
|
|
1365
|
+
this.emit("thinking", event.content);
|
|
1366
|
+
break;
|
|
1367
|
+
case "agent:tool_call": {
|
|
1368
|
+
const toolName = event.tool;
|
|
1369
|
+
const toolInput = (event.input ?? {});
|
|
1370
|
+
// Sandbox pre-validation: block disallowed tool calls
|
|
1371
|
+
if (this.sandboxManager) {
|
|
1372
|
+
const validation = this.sandboxManager.validateToolCall(toolName, toolInput);
|
|
1373
|
+
if (!validation.allowed) {
|
|
1374
|
+
this._logger.warn("system", `Sandbox blocked tool "${toolName}": ${validation.violations.join("; ")}`);
|
|
1375
|
+
// The event is still emitted for observability, but the
|
|
1376
|
+
// AgentLoop's toolExecutor wrapper handles the actual blocking.
|
|
1377
|
+
// We emit a synthetic tool_result error to signal the block.
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
this._logger.logToolCall(toolName, toolInput);
|
|
1381
|
+
this.emit("tool:call", toolName, toolInput);
|
|
1382
|
+
break;
|
|
1383
|
+
}
|
|
1384
|
+
case "agent:tool_result":
|
|
1385
|
+
this._logger.logToolResult(event.tool, event.output, event.durationMs ?? 0, event.success ?? true);
|
|
1386
|
+
this.emit("tool:result", event.tool, event.output);
|
|
1387
|
+
if (this.perfOptimizer) {
|
|
1388
|
+
this.perfOptimizer.recordToolCall(event.tool, event.input ?? {}, event.durationMs ?? 0);
|
|
1389
|
+
}
|
|
1390
|
+
break;
|
|
1391
|
+
case "agent:file_change":
|
|
1392
|
+
// Track changed files
|
|
1393
|
+
if (typeof event.path === "string") {
|
|
1394
|
+
this.changedFiles.add(event.path);
|
|
1395
|
+
}
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* analyze phase용 분석 프롬프트를 생성한다.
|
|
1402
|
+
*
|
|
1403
|
+
* @param goal - 사용자 목표
|
|
1404
|
+
* @param codebaseStats - CodebaseContext 통계
|
|
1405
|
+
* @returns 시스템 프롬프트
|
|
1406
|
+
*/
|
|
1407
|
+
buildAnalysisPrompt(goal, codebaseStats) {
|
|
1408
|
+
const statsStr = Object.keys(codebaseStats).length > 0
|
|
1409
|
+
? `\n## Codebase Statistics\n${JSON.stringify(codebaseStats, null, 2)}`
|
|
1410
|
+
: "";
|
|
1411
|
+
return `You are a code analysis expert. Analyze the user's request and the project context.
|
|
1412
|
+
${statsStr}
|
|
1413
|
+
|
|
1414
|
+
## Instructions
|
|
1415
|
+
1. Determine the complexity of the request: trivial, simple, moderate, complex, or massive
|
|
1416
|
+
2. Identify relevant files and patterns
|
|
1417
|
+
3. Note any risks or dependencies
|
|
1418
|
+
4. Provide a brief analysis summary
|
|
1419
|
+
|
|
1420
|
+
Start your response with the complexity level (e.g., "Complexity: moderate")
|
|
1421
|
+
Then provide your analysis.`;
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* 개별 step 실행을 위한 시스템 프롬프트를 생성한다.
|
|
1425
|
+
*
|
|
1426
|
+
* @param plan - 전체 실행 계획
|
|
1427
|
+
* @param step - 현재 step
|
|
1428
|
+
* @param stepIndex - step 인덱스
|
|
1429
|
+
* @returns 시스템 프롬프트
|
|
1430
|
+
*/
|
|
1431
|
+
buildStepPrompt(plan, step, stepIndex) {
|
|
1432
|
+
const sections = [];
|
|
1433
|
+
sections.push(`You are an expert coding agent executing step ${stepIndex + 1} of ${plan.steps.length}.`);
|
|
1434
|
+
sections.push("");
|
|
1435
|
+
sections.push(`## Overall Goal`);
|
|
1436
|
+
sections.push(plan.goal);
|
|
1437
|
+
sections.push("");
|
|
1438
|
+
sections.push(`## Current Step`);
|
|
1439
|
+
sections.push(step.goal);
|
|
1440
|
+
sections.push("");
|
|
1441
|
+
if (step.targetFiles.length > 0) {
|
|
1442
|
+
sections.push(`## Target Files`);
|
|
1443
|
+
sections.push(step.targetFiles.join(", "));
|
|
1444
|
+
sections.push("");
|
|
1445
|
+
}
|
|
1446
|
+
if (step.readFiles.length > 0) {
|
|
1447
|
+
sections.push(`## Reference Files (read only)`);
|
|
1448
|
+
sections.push(step.readFiles.join(", "));
|
|
1449
|
+
sections.push("");
|
|
1450
|
+
}
|
|
1451
|
+
sections.push(`## Instructions`);
|
|
1452
|
+
sections.push(`- Focus on this step's goal only`);
|
|
1453
|
+
sections.push(`- Read relevant files before making changes`);
|
|
1454
|
+
sections.push(`- Make precise, minimal changes`);
|
|
1455
|
+
sections.push(`- Verify your changes compile/work`);
|
|
1456
|
+
sections.push(`- When done, provide a summary of what you changed`);
|
|
1457
|
+
return sections.join("\n");
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* DeepVerifyResult를 StateMachine의 VerifyResult 형식으로 변환한다.
|
|
1461
|
+
*
|
|
1462
|
+
* @param deep - 6-dimension 심층 검증 결과
|
|
1463
|
+
* @returns StateMachine VerifyResult
|
|
1464
|
+
*/
|
|
1465
|
+
mapVerifyResult(deep) {
|
|
1466
|
+
const d = deep.dimensions;
|
|
1467
|
+
return {
|
|
1468
|
+
verdict: deep.verdict,
|
|
1469
|
+
checks: {
|
|
1470
|
+
buildSuccess: d.correctness.status !== "fail",
|
|
1471
|
+
typesSafe: d.correctness.status !== "fail",
|
|
1472
|
+
testsPass: d.completeness.status !== "fail",
|
|
1473
|
+
noRegressions: d.consistency.status !== "fail",
|
|
1474
|
+
followsPatterns: d.quality.status !== "fail",
|
|
1475
|
+
securityClean: d.security.status !== "fail",
|
|
1476
|
+
},
|
|
1477
|
+
issues: [
|
|
1478
|
+
...d.correctness.issues,
|
|
1479
|
+
...d.completeness.issues,
|
|
1480
|
+
...d.consistency.issues,
|
|
1481
|
+
...d.quality.issues,
|
|
1482
|
+
...d.security.issues,
|
|
1483
|
+
...d.performance.issues,
|
|
1484
|
+
],
|
|
1485
|
+
suggestions: deep.suggestedFixes.map((f) => `[${f.dimension}/${f.severity}] ${f.description}`),
|
|
1486
|
+
confidence: deep.confidence,
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* StateMachine 최종 상태에서 ExecutionResult를 생성한다.
|
|
1491
|
+
*
|
|
1492
|
+
* @param smState - StateMachine 최종 상태
|
|
1493
|
+
* @param startTime - 실행 시작 시각 (epoch ms)
|
|
1494
|
+
* @returns 실행 결과
|
|
1495
|
+
*/
|
|
1496
|
+
buildResult(smState, startTime) {
|
|
1497
|
+
const hasErrors = smState.errors.length > 0;
|
|
1498
|
+
const lastReflection = smState.reflections[smState.reflections.length - 1];
|
|
1499
|
+
const passed = !hasErrors ||
|
|
1500
|
+
(lastReflection && lastReflection.verdict === "pass");
|
|
1501
|
+
// Build summary from step results
|
|
1502
|
+
const summaryParts = [];
|
|
1503
|
+
for (const sr of smState.stepResults) {
|
|
1504
|
+
if (sr.phase === "implement" || sr.phase === "fix") {
|
|
1505
|
+
summaryParts.push(`[${sr.phase}] ${sr.output}`);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
const summary = summaryParts.length > 0
|
|
1509
|
+
? summaryParts.join("\n")
|
|
1510
|
+
: `Execution completed in phase: ${smState.phase}`;
|
|
1511
|
+
return {
|
|
1512
|
+
success: !!passed,
|
|
1513
|
+
termination: this.lastTermination,
|
|
1514
|
+
finalPhase: smState.phase,
|
|
1515
|
+
changedFiles: [...this.changedFiles],
|
|
1516
|
+
summary,
|
|
1517
|
+
totalTokens: {
|
|
1518
|
+
input: smState.tokenUsage.input,
|
|
1519
|
+
output: smState.tokenUsage.output,
|
|
1520
|
+
},
|
|
1521
|
+
totalIterations: smState.iterationCount,
|
|
1522
|
+
totalToolCalls: smState.toolCalls,
|
|
1523
|
+
durationMs: Date.now() - startTime,
|
|
1524
|
+
verifyResult: this.lastVerifyResult,
|
|
1525
|
+
monologue: [...this.reflection.getMonologue()],
|
|
1526
|
+
learnings: [...this.reflection.getAllLearnings()],
|
|
1527
|
+
plan: this.hierarchicalPlan,
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* JSON 문자열에서 JSON 블록을 추출한다.
|
|
1532
|
+
* markdown 코드 펜스 또는 순수 JSON을 처리한다.
|
|
1533
|
+
*
|
|
1534
|
+
* @param content - 원본 문자열
|
|
1535
|
+
* @returns 추출된 JSON 문자열
|
|
1536
|
+
*/
|
|
1537
|
+
extractJson(content) {
|
|
1538
|
+
// Strip markdown code fences
|
|
1539
|
+
const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
1540
|
+
if (fenced) {
|
|
1541
|
+
return fenced[1].trim();
|
|
1542
|
+
}
|
|
1543
|
+
// Find first JSON array or object
|
|
1544
|
+
const arrayMatch = content.match(/\[[\s\S]*\]/);
|
|
1545
|
+
if (arrayMatch) {
|
|
1546
|
+
return arrayMatch[0];
|
|
1547
|
+
}
|
|
1548
|
+
const objectMatch = content.match(/\{[\s\S]*\}/);
|
|
1549
|
+
if (objectMatch) {
|
|
1550
|
+
return objectMatch[0];
|
|
1551
|
+
}
|
|
1552
|
+
return content.trim();
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
//# sourceMappingURL=execution-engine.js.map
|