@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.
Files changed (235) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +15 -0
  3. package/dist/__tests__/context-manager.test.d.ts +6 -0
  4. package/dist/__tests__/context-manager.test.d.ts.map +1 -0
  5. package/dist/__tests__/context-manager.test.js +220 -0
  6. package/dist/__tests__/context-manager.test.js.map +1 -0
  7. package/dist/__tests__/governor.test.d.ts +6 -0
  8. package/dist/__tests__/governor.test.d.ts.map +1 -0
  9. package/dist/__tests__/governor.test.js +210 -0
  10. package/dist/__tests__/governor.test.js.map +1 -0
  11. package/dist/__tests__/model-router.test.d.ts +6 -0
  12. package/dist/__tests__/model-router.test.d.ts.map +1 -0
  13. package/dist/__tests__/model-router.test.js +329 -0
  14. package/dist/__tests__/model-router.test.js.map +1 -0
  15. package/dist/agent-logger.d.ts +384 -0
  16. package/dist/agent-logger.d.ts.map +1 -0
  17. package/dist/agent-logger.js +820 -0
  18. package/dist/agent-logger.js.map +1 -0
  19. package/dist/agent-loop.d.ts +163 -0
  20. package/dist/agent-loop.d.ts.map +1 -0
  21. package/dist/agent-loop.js +609 -0
  22. package/dist/agent-loop.js.map +1 -0
  23. package/dist/agent-modes.d.ts +85 -0
  24. package/dist/agent-modes.d.ts.map +1 -0
  25. package/dist/agent-modes.js +418 -0
  26. package/dist/agent-modes.js.map +1 -0
  27. package/dist/approval.d.ts +137 -0
  28. package/dist/approval.d.ts.map +1 -0
  29. package/dist/approval.js +299 -0
  30. package/dist/approval.js.map +1 -0
  31. package/dist/async-completion-queue.d.ts +56 -0
  32. package/dist/async-completion-queue.d.ts.map +1 -0
  33. package/dist/async-completion-queue.js +77 -0
  34. package/dist/async-completion-queue.js.map +1 -0
  35. package/dist/auto-fix.d.ts +174 -0
  36. package/dist/auto-fix.d.ts.map +1 -0
  37. package/dist/auto-fix.js +319 -0
  38. package/dist/auto-fix.js.map +1 -0
  39. package/dist/codebase-context.d.ts +396 -0
  40. package/dist/codebase-context.d.ts.map +1 -0
  41. package/dist/codebase-context.js +1260 -0
  42. package/dist/codebase-context.js.map +1 -0
  43. package/dist/conflict-resolver.d.ts +191 -0
  44. package/dist/conflict-resolver.d.ts.map +1 -0
  45. package/dist/conflict-resolver.js +524 -0
  46. package/dist/conflict-resolver.js.map +1 -0
  47. package/dist/constants.d.ts +52 -0
  48. package/dist/constants.d.ts.map +1 -0
  49. package/dist/constants.js +141 -0
  50. package/dist/constants.js.map +1 -0
  51. package/dist/context-budget.d.ts +435 -0
  52. package/dist/context-budget.d.ts.map +1 -0
  53. package/dist/context-budget.js +903 -0
  54. package/dist/context-budget.js.map +1 -0
  55. package/dist/context-compressor.d.ts +143 -0
  56. package/dist/context-compressor.d.ts.map +1 -0
  57. package/dist/context-compressor.js +511 -0
  58. package/dist/context-compressor.js.map +1 -0
  59. package/dist/context-manager.d.ts +112 -0
  60. package/dist/context-manager.d.ts.map +1 -0
  61. package/dist/context-manager.js +247 -0
  62. package/dist/context-manager.js.map +1 -0
  63. package/dist/continuous-reflection.d.ts +267 -0
  64. package/dist/continuous-reflection.d.ts.map +1 -0
  65. package/dist/continuous-reflection.js +338 -0
  66. package/dist/continuous-reflection.js.map +1 -0
  67. package/dist/cross-file-refactor.d.ts +352 -0
  68. package/dist/cross-file-refactor.d.ts.map +1 -0
  69. package/dist/cross-file-refactor.js +1544 -0
  70. package/dist/cross-file-refactor.js.map +1 -0
  71. package/dist/dag-orchestrator.d.ts +138 -0
  72. package/dist/dag-orchestrator.d.ts.map +1 -0
  73. package/dist/dag-orchestrator.js +379 -0
  74. package/dist/dag-orchestrator.js.map +1 -0
  75. package/dist/debate-orchestrator.d.ts +301 -0
  76. package/dist/debate-orchestrator.d.ts.map +1 -0
  77. package/dist/debate-orchestrator.js +719 -0
  78. package/dist/debate-orchestrator.js.map +1 -0
  79. package/dist/dependency-analyzer.d.ts +113 -0
  80. package/dist/dependency-analyzer.d.ts.map +1 -0
  81. package/dist/dependency-analyzer.js +444 -0
  82. package/dist/dependency-analyzer.js.map +1 -0
  83. package/dist/design-loop.d.ts +59 -0
  84. package/dist/design-loop.d.ts.map +1 -0
  85. package/dist/design-loop.js +344 -0
  86. package/dist/design-loop.js.map +1 -0
  87. package/dist/doc-intelligence.d.ts +383 -0
  88. package/dist/doc-intelligence.d.ts.map +1 -0
  89. package/dist/doc-intelligence.js +1307 -0
  90. package/dist/doc-intelligence.js.map +1 -0
  91. package/dist/dynamic-role-generator.d.ts +76 -0
  92. package/dist/dynamic-role-generator.d.ts.map +1 -0
  93. package/dist/dynamic-role-generator.js +194 -0
  94. package/dist/dynamic-role-generator.js.map +1 -0
  95. package/dist/errors.d.ts +69 -0
  96. package/dist/errors.d.ts.map +1 -0
  97. package/dist/errors.js +102 -0
  98. package/dist/errors.js.map +1 -0
  99. package/dist/event-bus.d.ts +159 -0
  100. package/dist/event-bus.d.ts.map +1 -0
  101. package/dist/event-bus.js +305 -0
  102. package/dist/event-bus.js.map +1 -0
  103. package/dist/execution-engine.d.ts +425 -0
  104. package/dist/execution-engine.d.ts.map +1 -0
  105. package/dist/execution-engine.js +1555 -0
  106. package/dist/execution-engine.js.map +1 -0
  107. package/dist/git-intelligence.d.ts +306 -0
  108. package/dist/git-intelligence.d.ts.map +1 -0
  109. package/dist/git-intelligence.js +1099 -0
  110. package/dist/git-intelligence.js.map +1 -0
  111. package/dist/governor.d.ts +77 -0
  112. package/dist/governor.d.ts.map +1 -0
  113. package/dist/governor.js +161 -0
  114. package/dist/governor.js.map +1 -0
  115. package/dist/hierarchical-planner.d.ts +313 -0
  116. package/dist/hierarchical-planner.d.ts.map +1 -0
  117. package/dist/hierarchical-planner.js +981 -0
  118. package/dist/hierarchical-planner.js.map +1 -0
  119. package/dist/index.d.ts +121 -0
  120. package/dist/index.d.ts.map +1 -0
  121. package/dist/index.js +123 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/intent-inference.d.ts +103 -0
  124. package/dist/intent-inference.d.ts.map +1 -0
  125. package/dist/intent-inference.js +605 -0
  126. package/dist/intent-inference.js.map +1 -0
  127. package/dist/interrupt-manager.d.ts +143 -0
  128. package/dist/interrupt-manager.d.ts.map +1 -0
  129. package/dist/interrupt-manager.js +196 -0
  130. package/dist/interrupt-manager.js.map +1 -0
  131. package/dist/kernel.d.ts +564 -0
  132. package/dist/kernel.d.ts.map +1 -0
  133. package/dist/kernel.js +1419 -0
  134. package/dist/kernel.js.map +1 -0
  135. package/dist/language-support.d.ts +232 -0
  136. package/dist/language-support.d.ts.map +1 -0
  137. package/dist/language-support.js +1134 -0
  138. package/dist/language-support.js.map +1 -0
  139. package/dist/llm-client.d.ts +82 -0
  140. package/dist/llm-client.d.ts.map +1 -0
  141. package/dist/llm-client.js +475 -0
  142. package/dist/llm-client.js.map +1 -0
  143. package/dist/mcp-client.d.ts +232 -0
  144. package/dist/mcp-client.d.ts.map +1 -0
  145. package/dist/mcp-client.js +718 -0
  146. package/dist/mcp-client.js.map +1 -0
  147. package/dist/memory-manager.d.ts +200 -0
  148. package/dist/memory-manager.d.ts.map +1 -0
  149. package/dist/memory-manager.js +568 -0
  150. package/dist/memory-manager.js.map +1 -0
  151. package/dist/memory.d.ts +87 -0
  152. package/dist/memory.d.ts.map +1 -0
  153. package/dist/memory.js +341 -0
  154. package/dist/memory.js.map +1 -0
  155. package/dist/model-router.d.ts +245 -0
  156. package/dist/model-router.d.ts.map +1 -0
  157. package/dist/model-router.js +632 -0
  158. package/dist/model-router.js.map +1 -0
  159. package/dist/parallel-executor.d.ts +125 -0
  160. package/dist/parallel-executor.d.ts.map +1 -0
  161. package/dist/parallel-executor.js +201 -0
  162. package/dist/parallel-executor.js.map +1 -0
  163. package/dist/perf-optimizer.d.ts +212 -0
  164. package/dist/perf-optimizer.d.ts.map +1 -0
  165. package/dist/perf-optimizer.js +721 -0
  166. package/dist/perf-optimizer.js.map +1 -0
  167. package/dist/persona.d.ts +305 -0
  168. package/dist/persona.d.ts.map +1 -0
  169. package/dist/persona.js +887 -0
  170. package/dist/persona.js.map +1 -0
  171. package/dist/planner.d.ts +70 -0
  172. package/dist/planner.d.ts.map +1 -0
  173. package/dist/planner.js +264 -0
  174. package/dist/planner.js.map +1 -0
  175. package/dist/qa-pipeline.d.ts +365 -0
  176. package/dist/qa-pipeline.d.ts.map +1 -0
  177. package/dist/qa-pipeline.js +1352 -0
  178. package/dist/qa-pipeline.js.map +1 -0
  179. package/dist/reasoning-adapter.d.ts +116 -0
  180. package/dist/reasoning-adapter.d.ts.map +1 -0
  181. package/dist/reasoning-adapter.js +187 -0
  182. package/dist/reasoning-adapter.js.map +1 -0
  183. package/dist/role-registry.d.ts +55 -0
  184. package/dist/role-registry.d.ts.map +1 -0
  185. package/dist/role-registry.js +192 -0
  186. package/dist/role-registry.js.map +1 -0
  187. package/dist/sandbox-tiers.d.ts +327 -0
  188. package/dist/sandbox-tiers.d.ts.map +1 -0
  189. package/dist/sandbox-tiers.js +928 -0
  190. package/dist/sandbox-tiers.js.map +1 -0
  191. package/dist/security-scanner.d.ts +222 -0
  192. package/dist/security-scanner.d.ts.map +1 -0
  193. package/dist/security-scanner.js +1129 -0
  194. package/dist/security-scanner.js.map +1 -0
  195. package/dist/security.d.ts +93 -0
  196. package/dist/security.d.ts.map +1 -0
  197. package/dist/security.js +393 -0
  198. package/dist/security.js.map +1 -0
  199. package/dist/self-reflection.d.ts +397 -0
  200. package/dist/self-reflection.d.ts.map +1 -0
  201. package/dist/self-reflection.js +908 -0
  202. package/dist/self-reflection.js.map +1 -0
  203. package/dist/session-persistence.d.ts +191 -0
  204. package/dist/session-persistence.d.ts.map +1 -0
  205. package/dist/session-persistence.js +395 -0
  206. package/dist/session-persistence.js.map +1 -0
  207. package/dist/speculative-executor.d.ts +210 -0
  208. package/dist/speculative-executor.d.ts.map +1 -0
  209. package/dist/speculative-executor.js +618 -0
  210. package/dist/speculative-executor.js.map +1 -0
  211. package/dist/state-machine.d.ts +289 -0
  212. package/dist/state-machine.d.ts.map +1 -0
  213. package/dist/state-machine.js +695 -0
  214. package/dist/state-machine.js.map +1 -0
  215. package/dist/sub-agent.d.ts +177 -0
  216. package/dist/sub-agent.d.ts.map +1 -0
  217. package/dist/sub-agent.js +303 -0
  218. package/dist/sub-agent.js.map +1 -0
  219. package/dist/system-prompt.d.ts +26 -0
  220. package/dist/system-prompt.d.ts.map +1 -0
  221. package/dist/system-prompt.js +84 -0
  222. package/dist/system-prompt.js.map +1 -0
  223. package/dist/test-intelligence.d.ts +439 -0
  224. package/dist/test-intelligence.d.ts.map +1 -0
  225. package/dist/test-intelligence.js +1165 -0
  226. package/dist/test-intelligence.js.map +1 -0
  227. package/dist/types.d.ts +632 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +6 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/vector-index.d.ts +314 -0
  232. package/dist/vector-index.d.ts.map +1 -0
  233. package/dist/vector-index.js +618 -0
  234. package/dist/vector-index.js.map +1 -0
  235. package/package.json +41 -0
package/dist/kernel.js ADDED
@@ -0,0 +1,1419 @@
1
+ /**
2
+ * @module kernel
3
+ * @description YUAN Agent Kernel — 4 Core Abstractions (SSOT)
4
+ *
5
+ * 1. AgentSession (KernelSession) — 실행 단위의 단일 진실 원본
6
+ * 2. PlanGraph (PlanGraphManager) — 계획의 운영 상태 관리
7
+ * 3. ToolContract (ToolContractRegistry) — 도구 계약 SSOT
8
+ * 4. EventLog — 모든 것을 이벤트로 기록
9
+ *
10
+ * 이 파일의 인터페이스와 클래스를 시스템 전체에서 SSOT로 참조한다.
11
+ */
12
+ /**
13
+ * PlanGraphManager — 계획 그래프의 상태 전이를 관리.
14
+ *
15
+ * - 노드 추가/제거
16
+ * - 의존성 기반 ready 판정
17
+ * - 상태 전이 (pending → ready → running → completed/failed/skipped)
18
+ * - 재시도 관리
19
+ * - 직렬화/역직렬화
20
+ */
21
+ export class PlanGraphManager {
22
+ state;
23
+ constructor(sessionId, goal) {
24
+ this.state = {
25
+ id: crypto.randomUUID(),
26
+ sessionId,
27
+ goal,
28
+ nodes: new Map(),
29
+ completedNodes: [],
30
+ runningNodes: [],
31
+ pendingNodes: [],
32
+ failedNodes: [],
33
+ skippedNodes: [],
34
+ totalTokensUsed: { input: 0, output: 0 },
35
+ startedAt: Date.now(),
36
+ estimatedCompletion: null,
37
+ criticalPath: [],
38
+ parallelGroups: [],
39
+ };
40
+ }
41
+ /**
42
+ * ExecutionPlan으로부터 PlanGraphManager를 생성.
43
+ * 각 PlanStep을 PlanNode로 변환하고 의존성을 연결한다.
44
+ */
45
+ static fromExecutionPlan(sessionId, plan) {
46
+ const mgr = new PlanGraphManager(sessionId, plan.goal);
47
+ for (const step of plan.steps) {
48
+ mgr.addNode({
49
+ id: step.id,
50
+ goal: step.goal,
51
+ targetFiles: step.targetFiles,
52
+ tools: step.tools,
53
+ dependsOn: step.dependsOn,
54
+ });
55
+ }
56
+ mgr.updateReadyNodes();
57
+ mgr.computeCriticalPath();
58
+ mgr.computeParallelGroups();
59
+ return mgr;
60
+ }
61
+ // ─── Node Management ───
62
+ /**
63
+ * 노드 추가. 런타임 상태 필드는 기본값으로 초기화.
64
+ */
65
+ addNode(input) {
66
+ const node = {
67
+ ...input,
68
+ status: "pending",
69
+ startedAt: null,
70
+ completedAt: null,
71
+ result: null,
72
+ error: null,
73
+ changedFiles: [],
74
+ tokensUsed: { input: 0, output: 0 },
75
+ attempts: 0,
76
+ maxAttempts: 3,
77
+ };
78
+ this.state.nodes.set(node.id, node);
79
+ this.rebuildComputedState();
80
+ }
81
+ // ─── State Transitions ───
82
+ /**
83
+ * 노드를 ready 상태로 전환 (의존성이 모두 완료됨).
84
+ */
85
+ markReady(nodeId) {
86
+ const node = this.requireNode(nodeId);
87
+ if (node.status !== "pending" && node.status !== "blocked") {
88
+ throw new Error(`Cannot mark node "${nodeId}" as ready: current status is "${node.status}"`);
89
+ }
90
+ node.status = "ready";
91
+ this.rebuildComputedState();
92
+ }
93
+ /**
94
+ * 노드를 running 상태로 전환 (실행 시작).
95
+ */
96
+ markRunning(nodeId) {
97
+ const node = this.requireNode(nodeId);
98
+ if (node.status !== "ready") {
99
+ throw new Error(`Cannot mark node "${nodeId}" as running: current status is "${node.status}"`);
100
+ }
101
+ node.status = "running";
102
+ node.startedAt = Date.now();
103
+ node.attempts += 1;
104
+ this.rebuildComputedState();
105
+ }
106
+ /**
107
+ * 노드를 completed 상태로 전환 (성공적으로 완료).
108
+ */
109
+ markCompleted(nodeId, result, changedFiles, tokensUsed) {
110
+ const node = this.requireNode(nodeId);
111
+ if (node.status !== "running") {
112
+ throw new Error(`Cannot mark node "${nodeId}" as completed: current status is "${node.status}"`);
113
+ }
114
+ node.status = "completed";
115
+ node.completedAt = Date.now();
116
+ node.result = result;
117
+ node.changedFiles = changedFiles;
118
+ node.tokensUsed = tokensUsed;
119
+ // 전체 토큰 사용량 업데이트
120
+ this.state.totalTokensUsed.input += tokensUsed.input;
121
+ this.state.totalTokensUsed.output += tokensUsed.output;
122
+ this.rebuildComputedState();
123
+ this.updateReadyNodes();
124
+ }
125
+ /**
126
+ * 노드를 failed 상태로 전환.
127
+ */
128
+ markFailed(nodeId, error) {
129
+ const node = this.requireNode(nodeId);
130
+ if (node.status !== "running") {
131
+ throw new Error(`Cannot mark node "${nodeId}" as failed: current status is "${node.status}"`);
132
+ }
133
+ node.status = "failed";
134
+ node.completedAt = Date.now();
135
+ node.error = error;
136
+ this.rebuildComputedState();
137
+ // 이 노드에 의존하는 노드들을 blocked로 전환
138
+ this.blockDependents(nodeId);
139
+ }
140
+ /**
141
+ * 노드를 skipped 상태로 전환.
142
+ */
143
+ markSkipped(nodeId, reason) {
144
+ const node = this.requireNode(nodeId);
145
+ node.status = "skipped";
146
+ node.completedAt = Date.now();
147
+ node.error = reason;
148
+ this.rebuildComputedState();
149
+ // 이 노드에 의존하는 노드들도 blocked
150
+ this.blockDependents(nodeId);
151
+ }
152
+ /**
153
+ * 실패한 노드를 재시도. maxAttempts 초과 시 false 반환.
154
+ */
155
+ retry(nodeId) {
156
+ const node = this.requireNode(nodeId);
157
+ if (node.status !== "failed") {
158
+ throw new Error(`Cannot retry node "${nodeId}": current status is "${node.status}"`);
159
+ }
160
+ if (node.attempts >= node.maxAttempts) {
161
+ return false;
162
+ }
163
+ // 상태를 ready로 되돌림 (markRunning에서 attempts 증가)
164
+ node.status = "ready";
165
+ node.error = null;
166
+ node.completedAt = null;
167
+ this.rebuildComputedState();
168
+ return true;
169
+ }
170
+ // ─── Queries ───
171
+ /**
172
+ * 의존성이 모두 완료되어 실행 가능한 노드 목록.
173
+ */
174
+ getReadyNodes() {
175
+ const result = [];
176
+ for (const node of this.state.nodes.values()) {
177
+ if (node.status === "ready") {
178
+ result.push(node);
179
+ }
180
+ }
181
+ return result;
182
+ }
183
+ /** 노드 조회 */
184
+ getNode(id) {
185
+ return this.state.nodes.get(id);
186
+ }
187
+ /** 전체 상태 (읽기 전용) */
188
+ getState() {
189
+ return this.state;
190
+ }
191
+ /**
192
+ * 모든 노드가 종료 상태(completed/failed/skipped)인지 확인.
193
+ */
194
+ isComplete() {
195
+ for (const node of this.state.nodes.values()) {
196
+ if (node.status !== "completed" &&
197
+ node.status !== "failed" &&
198
+ node.status !== "skipped") {
199
+ return false;
200
+ }
201
+ }
202
+ return this.state.nodes.size > 0;
203
+ }
204
+ /** 진행률 */
205
+ getProgress() {
206
+ const total = this.state.nodes.size;
207
+ const completed = this.state.completedNodes.length;
208
+ return {
209
+ completed,
210
+ total,
211
+ percent: total > 0 ? Math.round((completed / total) * 100) : 0,
212
+ };
213
+ }
214
+ /**
215
+ * 의존성 완료 후 ready 상태로 전환할 수 있는 노드를 업데이트.
216
+ * @returns 새로 ready가 된 노드 ID 목록
217
+ */
218
+ updateReadyNodes() {
219
+ const newlyReady = [];
220
+ for (const node of this.state.nodes.values()) {
221
+ if (node.status !== "pending")
222
+ continue;
223
+ const depsAllCompleted = node.dependsOn.every((depId) => {
224
+ const dep = this.state.nodes.get(depId);
225
+ return dep !== undefined && dep.status === "completed";
226
+ });
227
+ if (depsAllCompleted) {
228
+ node.status = "ready";
229
+ newlyReady.push(node.id);
230
+ }
231
+ }
232
+ if (newlyReady.length > 0) {
233
+ this.rebuildComputedState();
234
+ }
235
+ return newlyReady;
236
+ }
237
+ // ─── Serialization ───
238
+ /** JSON 직렬화 (Map/Set → 배열 변환) */
239
+ toJSON() {
240
+ const nodesArr = [];
241
+ for (const node of this.state.nodes.values()) {
242
+ nodesArr.push({ ...node });
243
+ }
244
+ return {
245
+ id: this.state.id,
246
+ sessionId: this.state.sessionId,
247
+ goal: this.state.goal,
248
+ nodes: nodesArr,
249
+ completedNodes: this.state.completedNodes,
250
+ runningNodes: this.state.runningNodes,
251
+ pendingNodes: this.state.pendingNodes,
252
+ failedNodes: this.state.failedNodes,
253
+ skippedNodes: this.state.skippedNodes,
254
+ totalTokensUsed: this.state.totalTokensUsed,
255
+ startedAt: this.state.startedAt,
256
+ estimatedCompletion: this.state.estimatedCompletion,
257
+ criticalPath: this.state.criticalPath,
258
+ parallelGroups: this.state.parallelGroups,
259
+ };
260
+ }
261
+ /** JSON에서 PlanGraphManager 복구 */
262
+ static fromJSON(json) {
263
+ const sessionId = json.sessionId;
264
+ const goal = json.goal;
265
+ const mgr = new PlanGraphManager(sessionId, goal);
266
+ mgr.state.id = json.id;
267
+ mgr.state.startedAt = json.startedAt;
268
+ mgr.state.estimatedCompletion =
269
+ json.estimatedCompletion ?? null;
270
+ mgr.state.totalTokensUsed = json.totalTokensUsed;
271
+ mgr.state.criticalPath = json.criticalPath ?? [];
272
+ mgr.state.parallelGroups = json.parallelGroups ?? [];
273
+ const nodesArr = json.nodes;
274
+ for (const node of nodesArr) {
275
+ mgr.state.nodes.set(node.id, { ...node });
276
+ }
277
+ mgr.rebuildComputedState();
278
+ return mgr;
279
+ }
280
+ // ─── Private Helpers ───
281
+ /** 노드 존재 확인 (없으면 throw) */
282
+ requireNode(nodeId) {
283
+ const node = this.state.nodes.get(nodeId);
284
+ if (!node) {
285
+ throw new Error(`Plan node not found: "${nodeId}"`);
286
+ }
287
+ return node;
288
+ }
289
+ /** 상태별 노드 목록 재계산 */
290
+ rebuildComputedState() {
291
+ this.state.completedNodes = [];
292
+ this.state.runningNodes = [];
293
+ this.state.pendingNodes = [];
294
+ this.state.failedNodes = [];
295
+ this.state.skippedNodes = [];
296
+ for (const node of this.state.nodes.values()) {
297
+ switch (node.status) {
298
+ case "completed":
299
+ this.state.completedNodes.push(node.id);
300
+ break;
301
+ case "running":
302
+ this.state.runningNodes.push(node.id);
303
+ break;
304
+ case "pending":
305
+ case "ready":
306
+ case "blocked":
307
+ this.state.pendingNodes.push(node.id);
308
+ break;
309
+ case "failed":
310
+ this.state.failedNodes.push(node.id);
311
+ break;
312
+ case "skipped":
313
+ this.state.skippedNodes.push(node.id);
314
+ break;
315
+ }
316
+ }
317
+ }
318
+ /** 실패/스킵된 노드에 의존하는 노드를 blocked 상태로 전환 */
319
+ blockDependents(failedNodeId) {
320
+ for (const node of this.state.nodes.values()) {
321
+ if (node.dependsOn.includes(failedNodeId) &&
322
+ (node.status === "pending" || node.status === "ready")) {
323
+ node.status = "blocked";
324
+ }
325
+ }
326
+ this.rebuildComputedState();
327
+ }
328
+ /** 크리티컬 패스 계산 (가장 긴 의존 체인) */
329
+ computeCriticalPath() {
330
+ const memo = new Map();
331
+ const longestPath = (nodeId, visited = new Set()) => {
332
+ if (memo.has(nodeId))
333
+ return memo.get(nodeId);
334
+ if (visited.has(nodeId))
335
+ return []; // cycle detected — break recursion
336
+ visited.add(nodeId);
337
+ const node = this.state.nodes.get(nodeId);
338
+ if (!node || node.dependsOn.length === 0) {
339
+ const path = [nodeId];
340
+ memo.set(nodeId, path);
341
+ return path;
342
+ }
343
+ let longest = [];
344
+ for (const depId of node.dependsOn) {
345
+ const depPath = longestPath(depId, visited);
346
+ if (depPath.length > longest.length) {
347
+ longest = depPath;
348
+ }
349
+ }
350
+ const path = [...longest, nodeId];
351
+ memo.set(nodeId, path);
352
+ return path;
353
+ };
354
+ let criticalPath = [];
355
+ for (const nodeId of this.state.nodes.keys()) {
356
+ const path = longestPath(nodeId);
357
+ if (path.length > criticalPath.length) {
358
+ criticalPath = path;
359
+ }
360
+ }
361
+ this.state.criticalPath = criticalPath;
362
+ }
363
+ /** 병렬 실행 가능한 노드 그룹 계산 (토폴로지 레벨별) */
364
+ computeParallelGroups() {
365
+ const levels = new Map();
366
+ const getLevel = (nodeId, visited = new Set()) => {
367
+ if (levels.has(nodeId))
368
+ return levels.get(nodeId);
369
+ if (visited.has(nodeId))
370
+ return 0; // cycle detected — break recursion
371
+ visited.add(nodeId);
372
+ const node = this.state.nodes.get(nodeId);
373
+ if (!node || node.dependsOn.length === 0) {
374
+ levels.set(nodeId, 0);
375
+ return 0;
376
+ }
377
+ let maxDepLevel = -1;
378
+ for (const depId of node.dependsOn) {
379
+ const depLevel = getLevel(depId, visited);
380
+ if (depLevel > maxDepLevel) {
381
+ maxDepLevel = depLevel;
382
+ }
383
+ }
384
+ const level = maxDepLevel + 1;
385
+ levels.set(nodeId, level);
386
+ return level;
387
+ };
388
+ for (const nodeId of this.state.nodes.keys()) {
389
+ getLevel(nodeId);
390
+ }
391
+ // 레벨별 그룹화
392
+ const groupMap = new Map();
393
+ for (const [nodeId, level] of levels) {
394
+ if (!groupMap.has(level)) {
395
+ groupMap.set(level, []);
396
+ }
397
+ groupMap.get(level).push(nodeId);
398
+ }
399
+ // 레벨 순서대로 정렬
400
+ const sortedLevels = Array.from(groupMap.keys()).sort((a, b) => a - b);
401
+ this.state.parallelGroups = sortedLevels.map((level) => groupMap.get(level));
402
+ }
403
+ }
404
+ /**
405
+ * ToolContractRegistry — 도구 계약 레지스트리.
406
+ *
407
+ * 모든 도구의 계약을 등록, 조회, 검증한다.
408
+ * LLM에게 전달할 ToolDefinition 목록도 여기서 생성.
409
+ */
410
+ export class ToolContractRegistry {
411
+ contracts = new Map();
412
+ /** 계약 등록 */
413
+ register(contract) {
414
+ this.contracts.set(contract.name, contract);
415
+ }
416
+ /** 계약 조회 */
417
+ get(name) {
418
+ return this.contracts.get(name);
419
+ }
420
+ /** 전체 계약 목록 */
421
+ getAll() {
422
+ return Array.from(this.contracts.values());
423
+ }
424
+ /**
425
+ * 도구 호출 입력을 계약에 대해 검증.
426
+ * @returns { valid, errors } — errors는 빈 배열이면 유효
427
+ */
428
+ validate(name, input) {
429
+ const contract = this.contracts.get(name);
430
+ if (!contract) {
431
+ return { valid: false, errors: [`Unknown tool: "${name}"`] };
432
+ }
433
+ const errors = [];
434
+ const schema = contract.inputSchema;
435
+ // required 필드 검증
436
+ for (const reqField of schema.required) {
437
+ if (input[reqField] === undefined || input[reqField] === null) {
438
+ errors.push(`Missing required field: "${reqField}"`);
439
+ }
440
+ }
441
+ // 속성별 타입/패턴/길이 검증
442
+ for (const [key, value] of Object.entries(input)) {
443
+ const propSchema = schema.properties[key];
444
+ if (!propSchema)
445
+ continue; // 미정의 속성은 무시
446
+ // enum 검증
447
+ if (propSchema.enum && !propSchema.enum.includes(String(value))) {
448
+ errors.push(`Field "${key}" must be one of: ${propSchema.enum.join(", ")}`);
449
+ }
450
+ // pattern 검증
451
+ if (propSchema.pattern && typeof value === "string") {
452
+ const regex = new RegExp(propSchema.pattern);
453
+ if (!regex.test(value)) {
454
+ errors.push(`Field "${key}" does not match pattern: ${propSchema.pattern}`);
455
+ }
456
+ }
457
+ // maxLength 검증
458
+ if (propSchema.maxLength !== undefined &&
459
+ typeof value === "string" &&
460
+ value.length > propSchema.maxLength) {
461
+ errors.push(`Field "${key}" exceeds max length of ${propSchema.maxLength}`);
462
+ }
463
+ }
464
+ // 보안 패턴 검증 (blockedPatterns)
465
+ const stringInput = JSON.stringify(input);
466
+ for (const pattern of contract.securityPolicy.blockedPatterns) {
467
+ const regex = new RegExp(pattern, "i");
468
+ if (regex.test(stringInput)) {
469
+ errors.push(`Input matches blocked security pattern: ${pattern}`);
470
+ }
471
+ }
472
+ return { valid: errors.length === 0, errors };
473
+ }
474
+ /**
475
+ * 도구 호출에 승인이 필요한지 판단.
476
+ * @param autoApproveSettings 자동 승인된 도구/설정 목록
477
+ */
478
+ needsApproval(name, _input, autoApproveSettings) {
479
+ const contract = this.contracts.get(name);
480
+ if (!contract)
481
+ return true; // 미등록 도구는 항상 승인 필요
482
+ if (!contract.approvalPolicy.requiresApproval)
483
+ return false;
484
+ // autoApproveSettings에 도구 이름이 있으면 승인 불필요
485
+ if (autoApproveSettings.includes(name))
486
+ return false;
487
+ // autoApproveConditions 평가
488
+ for (const cond of contract.approvalPolicy.autoApproveConditions) {
489
+ switch (cond.type) {
490
+ case "always":
491
+ return false;
492
+ case "same_tool":
493
+ if (autoApproveSettings.includes(`always:${name}`))
494
+ return false;
495
+ break;
496
+ case "below_risk":
497
+ if (cond.value) {
498
+ const riskOrder = ["low", "medium", "high", "critical"];
499
+ const threshold = riskOrder.indexOf(cond.value);
500
+ const current = riskOrder.indexOf(contract.approvalPolicy.risk);
501
+ if (current <= threshold)
502
+ return false;
503
+ }
504
+ break;
505
+ case "user_setting":
506
+ // user_setting은 외부에서 autoApproveSettings로 전달
507
+ break;
508
+ default:
509
+ break;
510
+ }
511
+ }
512
+ return true;
513
+ }
514
+ /**
515
+ * 특정 권한을 가진 도구 목록 필터링.
516
+ */
517
+ getToolsWithPermission(permission) {
518
+ return this.getAll().filter((c) => {
519
+ const val = c.permissions[permission];
520
+ // boolean 속성만 필터 (string[] 속성은 제외)
521
+ return typeof val === "boolean" && val === true;
522
+ });
523
+ }
524
+ /**
525
+ * LLM에 전달할 ToolDefinition 배열 생성.
526
+ */
527
+ toToolDefinitions() {
528
+ return this.getAll().map((c) => ({
529
+ name: c.name,
530
+ description: c.description,
531
+ parameters: {
532
+ type: "object",
533
+ properties: Object.fromEntries(Object.entries(c.inputSchema.properties).map(([key, prop]) => [
534
+ key,
535
+ {
536
+ type: prop.type,
537
+ description: prop.description,
538
+ ...(prop.enum ? { enum: prop.enum } : {}),
539
+ ...(prop.default !== undefined
540
+ ? { default: prop.default }
541
+ : {}),
542
+ },
543
+ ])),
544
+ required: c.inputSchema.required,
545
+ },
546
+ }));
547
+ }
548
+ /**
549
+ * YUAN 기본 9개 도구의 계약을 생성.
550
+ */
551
+ static createDefaultContracts() {
552
+ const registry = new ToolContractRegistry();
553
+ // ─── file_read ───
554
+ registry.register({
555
+ name: "file_read",
556
+ description: "Read file contents from the project directory",
557
+ inputSchema: {
558
+ type: "object",
559
+ properties: {
560
+ path: {
561
+ type: "string",
562
+ description: "Absolute or relative file path to read",
563
+ },
564
+ offset: {
565
+ type: "number",
566
+ description: "Line offset to start reading from (0-based)",
567
+ },
568
+ limit: {
569
+ type: "number",
570
+ description: "Maximum number of lines to read",
571
+ },
572
+ },
573
+ required: ["path"],
574
+ },
575
+ outputSchema: { type: "string", maxLength: 50000, truncateStrategy: "tail" },
576
+ permissions: {
577
+ fileRead: true,
578
+ fileWrite: false,
579
+ fileDelete: false,
580
+ shellExec: false,
581
+ networkAccess: false,
582
+ gitOps: false,
583
+ allowedPaths: [],
584
+ blockedPaths: ["**/.env", "**/*.pem", "**/*.key"],
585
+ },
586
+ approvalPolicy: {
587
+ requiresApproval: false,
588
+ autoApproveConditions: [{ type: "always" }],
589
+ risk: "low",
590
+ timeout: 0,
591
+ },
592
+ securityPolicy: {
593
+ sandboxTier: 0,
594
+ inputSanitization: true,
595
+ outputSanitization: true,
596
+ maxExecutionTime: 5000,
597
+ blockedPatterns: [],
598
+ auditLog: false,
599
+ },
600
+ constraints: {
601
+ maxCallsPerIteration: 20,
602
+ maxCallsPerSession: 500,
603
+ cooldownMs: 0,
604
+ parallelizable: true,
605
+ requiresProject: true,
606
+ supportedLanguages: [],
607
+ },
608
+ });
609
+ // ─── file_write ───
610
+ registry.register({
611
+ name: "file_write",
612
+ description: "Write or create a file in the project directory. Overwrites existing files.",
613
+ inputSchema: {
614
+ type: "object",
615
+ properties: {
616
+ path: {
617
+ type: "string",
618
+ description: "Absolute or relative file path to write",
619
+ },
620
+ content: {
621
+ type: "string",
622
+ description: "File content to write",
623
+ maxLength: 100000,
624
+ },
625
+ overwrite: {
626
+ type: "boolean",
627
+ description: "Whether to overwrite existing files",
628
+ default: false,
629
+ },
630
+ },
631
+ required: ["path", "content"],
632
+ },
633
+ outputSchema: { type: "string", maxLength: 500, truncateStrategy: "tail" },
634
+ permissions: {
635
+ fileRead: false,
636
+ fileWrite: true,
637
+ fileDelete: false,
638
+ shellExec: false,
639
+ networkAccess: false,
640
+ gitOps: false,
641
+ allowedPaths: [],
642
+ blockedPaths: [
643
+ "**/.env",
644
+ "**/*.pem",
645
+ "**/*.key",
646
+ "**/node_modules/**",
647
+ ],
648
+ },
649
+ approvalPolicy: {
650
+ requiresApproval: true,
651
+ autoApproveConditions: [
652
+ { type: "below_risk", value: "medium" },
653
+ ],
654
+ risk: "medium",
655
+ timeout: 120000,
656
+ },
657
+ securityPolicy: {
658
+ sandboxTier: 1,
659
+ inputSanitization: true,
660
+ outputSanitization: false,
661
+ maxExecutionTime: 10000,
662
+ blockedPatterns: [],
663
+ auditLog: true,
664
+ },
665
+ constraints: {
666
+ maxCallsPerIteration: 10,
667
+ maxCallsPerSession: 200,
668
+ cooldownMs: 0,
669
+ parallelizable: false,
670
+ requiresProject: true,
671
+ supportedLanguages: [],
672
+ },
673
+ });
674
+ // ─── file_edit ───
675
+ registry.register({
676
+ name: "file_edit",
677
+ description: "Edit an existing file by replacing specific text content (diff-based)",
678
+ inputSchema: {
679
+ type: "object",
680
+ properties: {
681
+ path: {
682
+ type: "string",
683
+ description: "File path to edit",
684
+ },
685
+ old_string: {
686
+ type: "string",
687
+ description: "Exact text to find and replace",
688
+ maxLength: 50000,
689
+ },
690
+ new_string: {
691
+ type: "string",
692
+ description: "Replacement text",
693
+ maxLength: 50000,
694
+ },
695
+ replace_all: {
696
+ type: "boolean",
697
+ description: "Replace all occurrences (default: false)",
698
+ default: false,
699
+ },
700
+ },
701
+ required: ["path", "old_string", "new_string"],
702
+ },
703
+ outputSchema: { type: "string", maxLength: 1000, truncateStrategy: "tail" },
704
+ permissions: {
705
+ fileRead: true,
706
+ fileWrite: true,
707
+ fileDelete: false,
708
+ shellExec: false,
709
+ networkAccess: false,
710
+ gitOps: false,
711
+ allowedPaths: [],
712
+ blockedPaths: [
713
+ "**/.env",
714
+ "**/*.pem",
715
+ "**/*.key",
716
+ "**/node_modules/**",
717
+ ],
718
+ },
719
+ approvalPolicy: {
720
+ requiresApproval: true,
721
+ autoApproveConditions: [
722
+ { type: "below_risk", value: "medium" },
723
+ ],
724
+ risk: "medium",
725
+ timeout: 120000,
726
+ },
727
+ securityPolicy: {
728
+ sandboxTier: 1,
729
+ inputSanitization: true,
730
+ outputSanitization: false,
731
+ maxExecutionTime: 10000,
732
+ blockedPatterns: [],
733
+ auditLog: true,
734
+ },
735
+ constraints: {
736
+ maxCallsPerIteration: 15,
737
+ maxCallsPerSession: 300,
738
+ cooldownMs: 0,
739
+ parallelizable: false,
740
+ requiresProject: true,
741
+ supportedLanguages: [],
742
+ },
743
+ });
744
+ // ─── shell_exec ───
745
+ registry.register({
746
+ name: "shell_exec",
747
+ description: "Execute a shell command in the project directory. Commands are validated for safety.",
748
+ inputSchema: {
749
+ type: "object",
750
+ properties: {
751
+ command: {
752
+ type: "string",
753
+ description: "Shell command to execute",
754
+ maxLength: 2000,
755
+ },
756
+ cwd: {
757
+ type: "string",
758
+ description: "Working directory (defaults to project root)",
759
+ },
760
+ timeout: {
761
+ type: "number",
762
+ description: "Execution timeout in milliseconds (default: 30000)",
763
+ default: 30000,
764
+ },
765
+ },
766
+ required: ["command"],
767
+ },
768
+ outputSchema: { type: "string", maxLength: 30000, truncateStrategy: "tail" },
769
+ permissions: {
770
+ fileRead: true,
771
+ fileWrite: true,
772
+ fileDelete: true,
773
+ shellExec: true,
774
+ networkAccess: false,
775
+ gitOps: false,
776
+ allowedPaths: [],
777
+ blockedPaths: [],
778
+ },
779
+ approvalPolicy: {
780
+ requiresApproval: true,
781
+ autoApproveConditions: [],
782
+ risk: "high",
783
+ timeout: 120000,
784
+ },
785
+ securityPolicy: {
786
+ sandboxTier: 3,
787
+ inputSanitization: true,
788
+ outputSanitization: true,
789
+ maxExecutionTime: 60000,
790
+ blockedPatterns: [
791
+ "rm\\s+-rf\\s+/",
792
+ "mkfs",
793
+ "dd\\s+if=",
794
+ ":(){ :|:& };:",
795
+ "curl.*\\|.*sh",
796
+ "wget.*\\|.*sh",
797
+ ],
798
+ auditLog: true,
799
+ },
800
+ constraints: {
801
+ maxCallsPerIteration: 5,
802
+ maxCallsPerSession: 100,
803
+ cooldownMs: 500,
804
+ parallelizable: false,
805
+ requiresProject: true,
806
+ supportedLanguages: [],
807
+ },
808
+ });
809
+ // ─── grep ───
810
+ registry.register({
811
+ name: "grep",
812
+ description: "Search file contents using regex patterns. Returns matching lines with context.",
813
+ inputSchema: {
814
+ type: "object",
815
+ properties: {
816
+ pattern: {
817
+ type: "string",
818
+ description: "Regular expression pattern to search for",
819
+ },
820
+ path: {
821
+ type: "string",
822
+ description: "Directory or file to search in",
823
+ },
824
+ glob: {
825
+ type: "string",
826
+ description: 'File glob pattern filter (e.g., "*.ts")',
827
+ },
828
+ context: {
829
+ type: "number",
830
+ description: "Number of context lines before and after each match",
831
+ default: 2,
832
+ },
833
+ max_results: {
834
+ type: "number",
835
+ description: "Maximum number of matching lines to return",
836
+ default: 50,
837
+ },
838
+ },
839
+ required: ["pattern"],
840
+ },
841
+ outputSchema: { type: "string", maxLength: 30000, truncateStrategy: "tail" },
842
+ permissions: {
843
+ fileRead: true,
844
+ fileWrite: false,
845
+ fileDelete: false,
846
+ shellExec: false,
847
+ networkAccess: false,
848
+ gitOps: false,
849
+ allowedPaths: [],
850
+ blockedPaths: ["**/.env", "**/*.pem", "**/*.key"],
851
+ },
852
+ approvalPolicy: {
853
+ requiresApproval: false,
854
+ autoApproveConditions: [{ type: "always" }],
855
+ risk: "low",
856
+ timeout: 0,
857
+ },
858
+ securityPolicy: {
859
+ sandboxTier: 0,
860
+ inputSanitization: true,
861
+ outputSanitization: true,
862
+ maxExecutionTime: 15000,
863
+ blockedPatterns: [],
864
+ auditLog: false,
865
+ },
866
+ constraints: {
867
+ maxCallsPerIteration: 20,
868
+ maxCallsPerSession: 500,
869
+ cooldownMs: 0,
870
+ parallelizable: true,
871
+ requiresProject: true,
872
+ supportedLanguages: [],
873
+ },
874
+ });
875
+ // ─── glob ───
876
+ registry.register({
877
+ name: "glob",
878
+ description: "Find files matching glob patterns in the project directory",
879
+ inputSchema: {
880
+ type: "object",
881
+ properties: {
882
+ pattern: {
883
+ type: "string",
884
+ description: 'Glob pattern (e.g., "**/*.ts", "src/**/*.tsx")',
885
+ },
886
+ path: {
887
+ type: "string",
888
+ description: "Base directory to search from",
889
+ },
890
+ },
891
+ required: ["pattern"],
892
+ },
893
+ outputSchema: { type: "string", maxLength: 20000, truncateStrategy: "tail" },
894
+ permissions: {
895
+ fileRead: true,
896
+ fileWrite: false,
897
+ fileDelete: false,
898
+ shellExec: false,
899
+ networkAccess: false,
900
+ gitOps: false,
901
+ allowedPaths: [],
902
+ blockedPaths: [],
903
+ },
904
+ approvalPolicy: {
905
+ requiresApproval: false,
906
+ autoApproveConditions: [{ type: "always" }],
907
+ risk: "low",
908
+ timeout: 0,
909
+ },
910
+ securityPolicy: {
911
+ sandboxTier: 0,
912
+ inputSanitization: true,
913
+ outputSanitization: false,
914
+ maxExecutionTime: 10000,
915
+ blockedPatterns: [],
916
+ auditLog: false,
917
+ },
918
+ constraints: {
919
+ maxCallsPerIteration: 15,
920
+ maxCallsPerSession: 300,
921
+ cooldownMs: 0,
922
+ parallelizable: true,
923
+ requiresProject: true,
924
+ supportedLanguages: [],
925
+ },
926
+ });
927
+ // ─── git_ops ───
928
+ registry.register({
929
+ name: "git_ops",
930
+ description: "Perform git operations (status, diff, add, commit, log, branch). Push requires approval.",
931
+ inputSchema: {
932
+ type: "object",
933
+ properties: {
934
+ operation: {
935
+ type: "string",
936
+ description: "Git operation to perform",
937
+ enum: [
938
+ "status",
939
+ "diff",
940
+ "add",
941
+ "commit",
942
+ "log",
943
+ "branch",
944
+ "push",
945
+ "stash",
946
+ "checkout",
947
+ ],
948
+ },
949
+ args: {
950
+ type: "string",
951
+ description: "Additional arguments for the git operation",
952
+ },
953
+ message: {
954
+ type: "string",
955
+ description: "Commit message (for commit operation)",
956
+ },
957
+ },
958
+ required: ["operation"],
959
+ },
960
+ outputSchema: { type: "string", maxLength: 20000, truncateStrategy: "tail" },
961
+ permissions: {
962
+ fileRead: true,
963
+ fileWrite: true,
964
+ fileDelete: false,
965
+ shellExec: true,
966
+ networkAccess: false,
967
+ gitOps: true,
968
+ allowedPaths: [],
969
+ blockedPaths: [],
970
+ },
971
+ approvalPolicy: {
972
+ requiresApproval: true,
973
+ autoApproveConditions: [
974
+ { type: "below_risk", value: "low" },
975
+ ],
976
+ risk: "medium",
977
+ timeout: 120000,
978
+ },
979
+ securityPolicy: {
980
+ sandboxTier: 2,
981
+ inputSanitization: true,
982
+ outputSanitization: true,
983
+ maxExecutionTime: 30000,
984
+ blockedPatterns: [
985
+ "force",
986
+ "--force",
987
+ "-f\\b",
988
+ "reset\\s+--hard",
989
+ ],
990
+ auditLog: true,
991
+ },
992
+ constraints: {
993
+ maxCallsPerIteration: 10,
994
+ maxCallsPerSession: 100,
995
+ cooldownMs: 200,
996
+ parallelizable: false,
997
+ requiresProject: true,
998
+ supportedLanguages: [],
999
+ },
1000
+ });
1001
+ // ─── test_run ───
1002
+ registry.register({
1003
+ name: "test_run",
1004
+ description: "Run project tests (unit, integration). Detects test framework automatically.",
1005
+ inputSchema: {
1006
+ type: "object",
1007
+ properties: {
1008
+ scope: {
1009
+ type: "string",
1010
+ description: 'Test scope: "all", "file", or "pattern"',
1011
+ enum: ["all", "file", "pattern"],
1012
+ default: "all",
1013
+ },
1014
+ target: {
1015
+ type: "string",
1016
+ description: "Test file path or pattern (for scope=file or scope=pattern)",
1017
+ },
1018
+ watch: {
1019
+ type: "boolean",
1020
+ description: "Run tests in watch mode",
1021
+ default: false,
1022
+ },
1023
+ },
1024
+ required: [],
1025
+ },
1026
+ outputSchema: { type: "string", maxLength: 30000, truncateStrategy: "tail" },
1027
+ permissions: {
1028
+ fileRead: true,
1029
+ fileWrite: false,
1030
+ fileDelete: false,
1031
+ shellExec: true,
1032
+ networkAccess: false,
1033
+ gitOps: false,
1034
+ allowedPaths: [],
1035
+ blockedPaths: [],
1036
+ },
1037
+ approvalPolicy: {
1038
+ requiresApproval: false,
1039
+ autoApproveConditions: [{ type: "always" }],
1040
+ risk: "low",
1041
+ timeout: 0,
1042
+ },
1043
+ securityPolicy: {
1044
+ sandboxTier: 2,
1045
+ inputSanitization: true,
1046
+ outputSanitization: true,
1047
+ maxExecutionTime: 120000,
1048
+ blockedPatterns: [],
1049
+ auditLog: true,
1050
+ },
1051
+ constraints: {
1052
+ maxCallsPerIteration: 3,
1053
+ maxCallsPerSession: 30,
1054
+ cooldownMs: 1000,
1055
+ parallelizable: false,
1056
+ requiresProject: true,
1057
+ supportedLanguages: [],
1058
+ },
1059
+ });
1060
+ // ─── security_scan ───
1061
+ registry.register({
1062
+ name: "security_scan",
1063
+ description: "Scan files or diffs for security issues (secrets, vulnerabilities, unsafe patterns)",
1064
+ inputSchema: {
1065
+ type: "object",
1066
+ properties: {
1067
+ target: {
1068
+ type: "string",
1069
+ description: "File path, directory, or 'staged' for git staged changes",
1070
+ },
1071
+ scan_type: {
1072
+ type: "string",
1073
+ description: "Type of scan to perform",
1074
+ enum: ["secrets", "vulnerabilities", "all"],
1075
+ default: "all",
1076
+ },
1077
+ },
1078
+ required: ["target"],
1079
+ },
1080
+ outputSchema: { type: "object", maxLength: 10000, truncateStrategy: "tail" },
1081
+ permissions: {
1082
+ fileRead: true,
1083
+ fileWrite: false,
1084
+ fileDelete: false,
1085
+ shellExec: false,
1086
+ networkAccess: false,
1087
+ gitOps: false,
1088
+ allowedPaths: [],
1089
+ blockedPaths: [],
1090
+ },
1091
+ approvalPolicy: {
1092
+ requiresApproval: false,
1093
+ autoApproveConditions: [{ type: "always" }],
1094
+ risk: "low",
1095
+ timeout: 0,
1096
+ },
1097
+ securityPolicy: {
1098
+ sandboxTier: 0,
1099
+ inputSanitization: true,
1100
+ outputSanitization: false,
1101
+ maxExecutionTime: 30000,
1102
+ blockedPatterns: [],
1103
+ auditLog: true,
1104
+ },
1105
+ constraints: {
1106
+ maxCallsPerIteration: 5,
1107
+ maxCallsPerSession: 50,
1108
+ cooldownMs: 0,
1109
+ parallelizable: true,
1110
+ requiresProject: true,
1111
+ supportedLanguages: [],
1112
+ },
1113
+ });
1114
+ return registry;
1115
+ }
1116
+ }
1117
+ /**
1118
+ * EventLog — 순서가 보장되는 이벤트 스트림 관리.
1119
+ *
1120
+ * - append: 이벤트 추가 (id, seq, timestamp 자동 설정)
1121
+ * - on: 타입별 구독 ("*"로 전체 구독)
1122
+ * - query: 타입/세션/노드/범위별 조회
1123
+ * - analysis: 타임라인, 도구 통계, 토큰 사용량
1124
+ * - replay: 비동기 이벤트 재생
1125
+ * - persistence: JSON 직렬화/역직렬화
1126
+ */
1127
+ export class EventLog {
1128
+ events = [];
1129
+ seqCounter = 0;
1130
+ maxSize;
1131
+ listeners = new Map();
1132
+ /**
1133
+ * @param maxSize 최대 이벤트 보관 수 (기본 10000)
1134
+ */
1135
+ constructor(maxSize = 10000) {
1136
+ this.maxSize = maxSize;
1137
+ }
1138
+ // ─── Append ───
1139
+ /**
1140
+ * 이벤트 추가. id, seq, timestamp를 자동으로 설정한다.
1141
+ * maxSize 초과 시 가장 오래된 이벤트를 제거.
1142
+ */
1143
+ append(event) {
1144
+ const fullEvent = {
1145
+ ...event,
1146
+ id: crypto.randomUUID(),
1147
+ seq: this.seqCounter++,
1148
+ timestamp: Date.now(),
1149
+ };
1150
+ this.events.push(fullEvent);
1151
+ // 용량 초과 시 오래된 이벤트 제거
1152
+ if (this.events.length > this.maxSize) {
1153
+ const excess = this.events.length - this.maxSize;
1154
+ this.events.splice(0, excess);
1155
+ }
1156
+ // 리스너 통지
1157
+ this.notifyListeners(fullEvent);
1158
+ return fullEvent;
1159
+ }
1160
+ // ─── Subscribe ───
1161
+ /**
1162
+ * 이벤트 구독. "*"로 모든 이벤트를 구독할 수 있다.
1163
+ * @returns 구독 해제 함수
1164
+ */
1165
+ on(type, listener) {
1166
+ if (!this.listeners.has(type)) {
1167
+ this.listeners.set(type, new Set());
1168
+ }
1169
+ this.listeners.get(type).add(listener);
1170
+ return () => {
1171
+ const set = this.listeners.get(type);
1172
+ if (set) {
1173
+ set.delete(listener);
1174
+ if (set.size === 0) {
1175
+ this.listeners.delete(type);
1176
+ }
1177
+ }
1178
+ };
1179
+ }
1180
+ // ─── Query ───
1181
+ /** 전체 이벤트 목록 (읽기 전용) */
1182
+ getAll() {
1183
+ return this.events;
1184
+ }
1185
+ /** 타입별 이벤트 조회 */
1186
+ getByType(type) {
1187
+ return this.events.filter((e) => e.type === type);
1188
+ }
1189
+ /** 세션별 이벤트 조회 */
1190
+ getBySession(sessionId) {
1191
+ return this.events.filter((e) => e.sessionId === sessionId);
1192
+ }
1193
+ /** 계획 노드별 이벤트 조회 */
1194
+ getByPlanNode(nodeId) {
1195
+ return this.events.filter((e) => e.planNodeId === nodeId);
1196
+ }
1197
+ /** 시퀀스 범위로 이벤트 조회 */
1198
+ getRange(fromSeq, toSeq) {
1199
+ return this.events.filter((e) => e.seq >= fromSeq && (toSeq === undefined || e.seq <= toSeq));
1200
+ }
1201
+ /** 마지막 N개 이벤트 조회 */
1202
+ getLast(n) {
1203
+ if (n <= 0)
1204
+ return [];
1205
+ return this.events.slice(-n);
1206
+ }
1207
+ // ─── Analysis ───
1208
+ /**
1209
+ * 세션 타임라인 — 이벤트를 phase 전이 기준으로 그룹화.
1210
+ */
1211
+ getTimeline(sessionId) {
1212
+ const sessionEvents = this.getBySession(sessionId);
1213
+ if (sessionEvents.length === 0)
1214
+ return [];
1215
+ const phases = [];
1216
+ let currentPhase = "init";
1217
+ let phaseEvents = [];
1218
+ let phaseStart = sessionEvents[0].timestamp;
1219
+ for (const event of sessionEvents) {
1220
+ // phase 전환 감지: session:start, plan:created, tool:call 등의 패턴
1221
+ const newPhase = this.detectPhase(event);
1222
+ if (newPhase && newPhase !== currentPhase) {
1223
+ // 이전 phase 마감
1224
+ if (phaseEvents.length > 0) {
1225
+ phases.push({
1226
+ phase: currentPhase,
1227
+ events: phaseEvents,
1228
+ duration: event.timestamp - phaseStart,
1229
+ });
1230
+ }
1231
+ currentPhase = newPhase;
1232
+ phaseEvents = [];
1233
+ phaseStart = event.timestamp;
1234
+ }
1235
+ phaseEvents.push(event);
1236
+ }
1237
+ // 마지막 phase 마감
1238
+ if (phaseEvents.length > 0) {
1239
+ const lastTs = phaseEvents[phaseEvents.length - 1].timestamp;
1240
+ phases.push({
1241
+ phase: currentPhase,
1242
+ events: phaseEvents,
1243
+ duration: lastTs - phaseStart,
1244
+ });
1245
+ }
1246
+ return phases;
1247
+ }
1248
+ /**
1249
+ * 에이전트 의사결정 로그 (type === "agent:decision").
1250
+ */
1251
+ getDecisionLog(sessionId) {
1252
+ return this.events.filter((e) => e.sessionId === sessionId && e.type === "agent:decision");
1253
+ }
1254
+ /**
1255
+ * 도구별 통계 — 호출 수, 에러 수, 평균 실행 시간.
1256
+ */
1257
+ getToolStats(sessionId) {
1258
+ const toolMap = new Map();
1259
+ const sessionEvents = this.getBySession(sessionId);
1260
+ for (const event of sessionEvents) {
1261
+ if (!event.toolName)
1262
+ continue;
1263
+ if (!toolMap.has(event.toolName)) {
1264
+ toolMap.set(event.toolName, {
1265
+ calls: 0,
1266
+ errors: 0,
1267
+ totalDuration: 0,
1268
+ });
1269
+ }
1270
+ const stats = toolMap.get(event.toolName);
1271
+ if (event.type === "tool:call") {
1272
+ stats.calls += 1;
1273
+ }
1274
+ else if (event.type === "tool:error") {
1275
+ stats.errors += 1;
1276
+ }
1277
+ else if (event.type === "tool:result") {
1278
+ const duration = typeof event.data.durationMs === "number"
1279
+ ? event.data.durationMs
1280
+ : 0;
1281
+ stats.totalDuration += duration;
1282
+ }
1283
+ }
1284
+ return Array.from(toolMap.entries()).map(([tool, stats]) => ({
1285
+ tool,
1286
+ calls: stats.calls,
1287
+ errors: stats.errors,
1288
+ avgDuration: stats.calls > 0
1289
+ ? Math.round(stats.totalDuration / stats.calls)
1290
+ : 0,
1291
+ }));
1292
+ }
1293
+ /**
1294
+ * 세션별 토큰 사용량 — 전체 + phase별 분류.
1295
+ */
1296
+ getTokenUsage(sessionId) {
1297
+ let total = 0;
1298
+ const byPhase = {};
1299
+ const timeline = this.getTimeline(sessionId);
1300
+ for (const phase of timeline) {
1301
+ let phaseTokens = 0;
1302
+ for (const event of phase.events) {
1303
+ if (event.tokenCost !== undefined) {
1304
+ phaseTokens += event.tokenCost;
1305
+ total += event.tokenCost;
1306
+ }
1307
+ }
1308
+ if (phaseTokens > 0) {
1309
+ byPhase[phase.phase] = phaseTokens;
1310
+ }
1311
+ }
1312
+ return { total, byPhase };
1313
+ }
1314
+ // ─── Replay ───
1315
+ /**
1316
+ * 비동기 이벤트 재생 — 지정 시퀀스부터 이벤트를 순서대로 yield.
1317
+ * @param fromSeq 시작 시퀀스 번호
1318
+ * @param speed 재생 속도 배율 (1 = 실시간, 2 = 2배속). 0이면 즉시.
1319
+ */
1320
+ async *replay(fromSeq, speed = 0) {
1321
+ const events = this.getRange(fromSeq);
1322
+ let prevTimestamp = null;
1323
+ for (const event of events) {
1324
+ // 속도 배율에 따라 이벤트 간 대기
1325
+ if (speed > 0 && prevTimestamp !== null) {
1326
+ const delay = (event.timestamp - prevTimestamp) / speed;
1327
+ if (delay > 0) {
1328
+ await new Promise((resolve) => setTimeout(resolve, delay));
1329
+ }
1330
+ }
1331
+ prevTimestamp = event.timestamp;
1332
+ yield event;
1333
+ }
1334
+ }
1335
+ // ─── Persistence ───
1336
+ /** JSON 직렬화 */
1337
+ toJSON() {
1338
+ return {
1339
+ events: this.events,
1340
+ seqCounter: this.seqCounter,
1341
+ maxSize: this.maxSize,
1342
+ };
1343
+ }
1344
+ /** JSON에서 EventLog 복구 */
1345
+ static fromJSON(json) {
1346
+ const maxSize = json.maxSize ?? 10000;
1347
+ const log = new EventLog(maxSize);
1348
+ log.events = json.events ?? [];
1349
+ log.seqCounter = json.seqCounter ?? 0;
1350
+ return log;
1351
+ }
1352
+ // ─── Cleanup ───
1353
+ /**
1354
+ * 오래된 이벤트를 제거.
1355
+ * @param keepLast 유지할 최근 이벤트 수 (기본: maxSize의 절반)
1356
+ * @returns 제거된 이벤트 수
1357
+ */
1358
+ prune(keepLast) {
1359
+ const keep = keepLast ?? Math.floor(this.maxSize / 2);
1360
+ if (this.events.length <= keep)
1361
+ return 0;
1362
+ const removeCount = this.events.length - keep;
1363
+ this.events.splice(0, removeCount);
1364
+ return removeCount;
1365
+ }
1366
+ // ─── Private Helpers ───
1367
+ /** 리스너에게 이벤트 통지 */
1368
+ notifyListeners(event) {
1369
+ // 타입별 리스너
1370
+ const typeListeners = this.listeners.get(event.type);
1371
+ if (typeListeners) {
1372
+ for (const listener of typeListeners) {
1373
+ try {
1374
+ listener(event);
1375
+ }
1376
+ catch {
1377
+ // 리스너 에러는 무시 (이벤트 시스템의 안정성 우선)
1378
+ }
1379
+ }
1380
+ }
1381
+ // 와일드카드 리스너
1382
+ const allListeners = this.listeners.get("*");
1383
+ if (allListeners) {
1384
+ for (const listener of allListeners) {
1385
+ try {
1386
+ listener(event);
1387
+ }
1388
+ catch {
1389
+ // 리스너 에러는 무시
1390
+ }
1391
+ }
1392
+ }
1393
+ }
1394
+ /** 이벤트 타입에서 phase 감지 */
1395
+ detectPhase(event) {
1396
+ switch (event.type) {
1397
+ case "session:start":
1398
+ return "start";
1399
+ case "plan:created":
1400
+ return "planning";
1401
+ case "plan:node_running":
1402
+ case "tool:call":
1403
+ return "executing";
1404
+ case "verify:start":
1405
+ return "verifying";
1406
+ case "plan:replan":
1407
+ return "replanning";
1408
+ case "session:complete":
1409
+ return "complete";
1410
+ case "session:fail":
1411
+ return "failed";
1412
+ case "session:stop":
1413
+ return "stopped";
1414
+ default:
1415
+ return null;
1416
+ }
1417
+ }
1418
+ }
1419
+ //# sourceMappingURL=kernel.js.map