activo 0.4.4 → 0.5.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 (161) hide show
  1. package/README.md +203 -1
  2. package/data/2026-03-04_20-54.json +181 -0
  3. package/data/2026-03-04_20-56.json +181 -0
  4. package/data/apex-rulesets/egov.yaml +469 -0
  5. package/data/apex-rulesets/modernize.yaml +687 -0
  6. package/data/apex-rulesets/quality.yaml +1677 -0
  7. package/data/apex-rulesets/rule-schema.yaml +587 -0
  8. package/data/apex-rulesets/secure.yaml +1688 -0
  9. package/data/apex-rulesets/spring.yaml +455 -0
  10. package/data/apex-rulesets/sql-format.yaml +99 -0
  11. package/data/apex-rulesets/sql-oracle.yaml +281 -0
  12. package/data/apex-rulesets/sql.yaml +1660 -0
  13. package/dist/cli/headless.d.ts.map +1 -1
  14. package/dist/cli/headless.js +32 -10
  15. package/dist/cli/headless.js.map +1 -1
  16. package/dist/cli/index.js +31 -3
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/core/agent.d.ts +3 -3
  19. package/dist/core/agent.d.ts.map +1 -1
  20. package/dist/core/agent.js +203 -384
  21. package/dist/core/agent.js.map +1 -1
  22. package/dist/core/commands.d.ts +2 -1
  23. package/dist/core/commands.d.ts.map +1 -1
  24. package/dist/core/commands.js +61 -9
  25. package/dist/core/commands.js.map +1 -1
  26. package/dist/core/config.d.ts +14 -0
  27. package/dist/core/config.d.ts.map +1 -1
  28. package/dist/core/config.js +41 -4
  29. package/dist/core/config.js.map +1 -1
  30. package/dist/core/conversation.d.ts +2 -2
  31. package/dist/core/conversation.d.ts.map +1 -1
  32. package/dist/core/conversation.js.map +1 -1
  33. package/dist/core/intentRouter.d.ts +43 -0
  34. package/dist/core/intentRouter.d.ts.map +1 -0
  35. package/dist/core/intentRouter.js +804 -0
  36. package/dist/core/intentRouter.js.map +1 -0
  37. package/dist/core/llm/anthropic.d.ts +24 -0
  38. package/dist/core/llm/anthropic.d.ts.map +1 -0
  39. package/dist/core/llm/anthropic.js +226 -0
  40. package/dist/core/llm/anthropic.js.map +1 -0
  41. package/dist/core/llm/ollama.d.ts +5 -14
  42. package/dist/core/llm/ollama.d.ts.map +1 -1
  43. package/dist/core/llm/ollama.js +3 -0
  44. package/dist/core/llm/ollama.js.map +1 -1
  45. package/dist/core/llm/types.d.ts +22 -0
  46. package/dist/core/llm/types.d.ts.map +1 -0
  47. package/dist/core/llm/types.js +2 -0
  48. package/dist/core/llm/types.js.map +1 -0
  49. package/dist/core/mcp/client.d.ts +6 -0
  50. package/dist/core/mcp/client.d.ts.map +1 -1
  51. package/dist/core/mcp/client.js +16 -0
  52. package/dist/core/mcp/client.js.map +1 -1
  53. package/dist/core/mcp/init.d.ts +12 -0
  54. package/dist/core/mcp/init.d.ts.map +1 -0
  55. package/dist/core/mcp/init.js +55 -0
  56. package/dist/core/mcp/init.js.map +1 -0
  57. package/dist/core/mcp/logger.d.ts +14 -0
  58. package/dist/core/mcp/logger.d.ts.map +1 -0
  59. package/dist/core/mcp/logger.js +50 -0
  60. package/dist/core/mcp/logger.js.map +1 -0
  61. package/dist/core/tools/analyzePatterns.d.ts +3 -0
  62. package/dist/core/tools/analyzePatterns.d.ts.map +1 -0
  63. package/dist/core/tools/analyzePatterns.js +293 -0
  64. package/dist/core/tools/analyzePatterns.js.map +1 -0
  65. package/dist/core/tools/apexPaths.d.ts +14 -0
  66. package/dist/core/tools/apexPaths.d.ts.map +1 -0
  67. package/dist/core/tools/apexPaths.js +54 -0
  68. package/dist/core/tools/apexPaths.js.map +1 -0
  69. package/dist/core/tools/apexUtils.d.ts +36 -0
  70. package/dist/core/tools/apexUtils.d.ts.map +1 -0
  71. package/dist/core/tools/apexUtils.js +83 -0
  72. package/dist/core/tools/apexUtils.js.map +1 -0
  73. package/dist/core/tools/explainIssue.d.ts +3 -0
  74. package/dist/core/tools/explainIssue.d.ts.map +1 -0
  75. package/dist/core/tools/explainIssue.js +181 -0
  76. package/dist/core/tools/explainIssue.js.map +1 -0
  77. package/dist/core/tools/fixGen.d.ts +3 -0
  78. package/dist/core/tools/fixGen.d.ts.map +1 -0
  79. package/dist/core/tools/fixGen.js +338 -0
  80. package/dist/core/tools/fixGen.js.map +1 -0
  81. package/dist/core/tools/generateImprovements.d.ts +21 -0
  82. package/dist/core/tools/generateImprovements.d.ts.map +1 -0
  83. package/dist/core/tools/generateImprovements.js +602 -0
  84. package/dist/core/tools/generateImprovements.js.map +1 -0
  85. package/dist/core/tools/generateReport.d.ts +3 -0
  86. package/dist/core/tools/generateReport.d.ts.map +1 -0
  87. package/dist/core/tools/generateReport.js +315 -0
  88. package/dist/core/tools/generateReport.js.map +1 -0
  89. package/dist/core/tools/index.d.ts +7 -0
  90. package/dist/core/tools/index.d.ts.map +1 -1
  91. package/dist/core/tools/index.js +62 -23
  92. package/dist/core/tools/index.js.map +1 -1
  93. package/dist/core/tools/recommendProfile.d.ts +3 -0
  94. package/dist/core/tools/recommendProfile.d.ts.map +1 -0
  95. package/dist/core/tools/recommendProfile.js +334 -0
  96. package/dist/core/tools/recommendProfile.js.map +1 -0
  97. package/dist/core/tools/ruleGen.d.ts +3 -0
  98. package/dist/core/tools/ruleGen.d.ts.map +1 -0
  99. package/dist/core/tools/ruleGen.js +1103 -0
  100. package/dist/core/tools/ruleGen.js.map +1 -0
  101. package/dist/core/tools/standards.d.ts.map +1 -1
  102. package/dist/core/tools/standards.js +7 -3
  103. package/dist/core/tools/standards.js.map +1 -1
  104. package/dist/ui/App.d.ts.map +1 -1
  105. package/dist/ui/App.js +86 -35
  106. package/dist/ui/App.js.map +1 -1
  107. package/dist/ui/components/InputBox.d.ts +1 -3
  108. package/dist/ui/components/InputBox.d.ts.map +1 -1
  109. package/dist/ui/components/InputBox.js +146 -5
  110. package/dist/ui/components/InputBox.js.map +1 -1
  111. package/dist/ui/components/MessageList.d.ts +3 -1
  112. package/dist/ui/components/MessageList.d.ts.map +1 -1
  113. package/dist/ui/components/MessageList.js +13 -7
  114. package/dist/ui/components/MessageList.js.map +1 -1
  115. package/dist/ui/components/StatusBar.d.ts +1 -1
  116. package/dist/ui/components/StatusBar.d.ts.map +1 -1
  117. package/dist/ui/components/StatusBar.js +3 -2
  118. package/dist/ui/components/StatusBar.js.map +1 -1
  119. package/dist/ui/components/ToolStatus.d.ts +3 -1
  120. package/dist/ui/components/ToolStatus.d.ts.map +1 -1
  121. package/dist/ui/components/ToolStatus.js +19 -4
  122. package/dist/ui/components/ToolStatus.js.map +1 -1
  123. package/package.json +7 -1
  124. package/demo.gif +0 -0
  125. package/demo.tape +0 -53
  126. package/screenshot.png +0 -0
  127. package/src/cli/banner.ts +0 -38
  128. package/src/cli/headless.ts +0 -63
  129. package/src/cli/index.ts +0 -57
  130. package/src/core/agent.ts +0 -711
  131. package/src/core/commands.ts +0 -118
  132. package/src/core/config.ts +0 -98
  133. package/src/core/conversation.ts +0 -235
  134. package/src/core/llm/ollama.ts +0 -351
  135. package/src/core/mcp/client.ts +0 -143
  136. package/src/core/tools/analyzeAll.ts +0 -482
  137. package/src/core/tools/ast.ts +0 -826
  138. package/src/core/tools/builtIn.ts +0 -221
  139. package/src/core/tools/cache.ts +0 -570
  140. package/src/core/tools/cssAnalysis.ts +0 -324
  141. package/src/core/tools/dependencyAnalysis.ts +0 -363
  142. package/src/core/tools/embeddings.ts +0 -746
  143. package/src/core/tools/frontendAst.ts +0 -802
  144. package/src/core/tools/htmlAnalysis.ts +0 -466
  145. package/src/core/tools/index.ts +0 -160
  146. package/src/core/tools/javaAst.ts +0 -1030
  147. package/src/core/tools/javaQuality.integration.test.ts +0 -537
  148. package/src/core/tools/memory.ts +0 -655
  149. package/src/core/tools/mybatisAnalysis.ts +0 -322
  150. package/src/core/tools/openapiAnalysis.ts +0 -431
  151. package/src/core/tools/pythonAnalysis.ts +0 -477
  152. package/src/core/tools/sqlAnalysis.ts +0 -298
  153. package/src/core/tools/standards.test.ts +0 -186
  154. package/src/core/tools/standards.ts +0 -889
  155. package/src/core/tools/types.ts +0 -38
  156. package/src/ui/App.tsx +0 -334
  157. package/src/ui/components/InputBox.tsx +0 -37
  158. package/src/ui/components/MessageList.tsx +0 -80
  159. package/src/ui/components/StatusBar.tsx +0 -36
  160. package/src/ui/components/ToolStatus.tsx +0 -38
  161. package/tsconfig.json +0 -21
@@ -1,12 +1,34 @@
1
- import * as fs from "fs";
2
1
  import { selectTools, executeTool } from "./tools/index.js";
2
+ import { detectAndExecuteIntent, compressAnalysisResult } from "./intentRouter.js";
3
+ // Extract a unique signature from a tool call for loop detection
4
+ function toolSignature(tc) {
5
+ // 모든 인자를 포함하여 false positive 방지
6
+ // (같은 도구를 다른 profile/pattern/args으로 호출하는 것은 반복이 아님)
7
+ const sortedArgs = Object.keys(tc.arguments)
8
+ .sort()
9
+ .map((key) => `${key}=${String(tc.arguments[key])}`)
10
+ .join(",");
11
+ return `${tc.name}:${sortedArgs}`;
12
+ }
3
13
  const BASE_SYSTEM_PROMPT = `You are ACTIVO, a code quality analyzer. You MUST call tools to perform tasks.
4
14
 
5
15
  ## RULES
6
16
  1. Call tool IMMEDIATELY when user requests an action
7
17
  2. NEVER fabricate results - only report actual tool output
8
18
  3. After tool returns, summarize in user's language (Korean if user speaks Korean)
9
- 4. Use analyze_all for broad code analysis`;
19
+ 4. Use analyze_all for broad code analysis
20
+ 5. When all tools have completed, STOP calling tools and provide a final summary
21
+
22
+ ## WORKFLOWS
23
+ - PDF→규칙: import_pdf_standards → generate_apex_rules (do NOT read_file individually)
24
+ - 코드분석: recommend_profile → mcp_apex_analyze_code → analyze_patterns
25
+ - 리포트: mcp_apex_analyze_code → generate_report (Excel 자동 포함)
26
+ - 엑셀출력: mcp_apex_analyze_code → mcp_apex_export_excel (analyze_code 후 즉시 엑셀 생성 가능)
27
+ - 전체보고서: mcp_apex_analyze_code → generate_report → generate_improvement_report`;
28
+ // System prompt for summary-only mode (no tool calling)
29
+ const SUMMARY_SYSTEM_PROMPT = `You are ACTIVO, a code quality analyzer.
30
+ You are summarizing tool results. Do NOT call any tools. Do NOT output XML or tool_use tags.
31
+ Just provide a clear, helpful summary in the user's language (Korean if user speaks Korean).`;
10
32
  // Build system prompt with optional context
11
33
  function buildSystemPrompt(contextSummary) {
12
34
  if (!contextSummary) {
@@ -21,353 +43,6 @@ ${contextSummary}
21
43
  ---
22
44
  위 내용은 이전 세션에서의 대화 요약입니다. 필요시 참고하세요.`;
23
45
  }
24
- // Intent patterns: keyword groups → tool + args builder
25
- const INTENT_PATTERNS = [
26
- // Single file analysis (must come before directory patterns)
27
- {
28
- keywords: ["분석", "analyze", "검사", "check"],
29
- tool: "_single_file", // special marker - resolved at match time
30
- buildArgs: (path) => ({ filepath: path }),
31
- },
32
- // analyze_all with Java filter
33
- {
34
- keywords: ["자바", "java"],
35
- tool: "analyze_all",
36
- buildArgs: (path) => ({ path, include: ["java"] }),
37
- },
38
- // Spring patterns
39
- {
40
- keywords: ["spring", "스프링"],
41
- tool: "analyze_all",
42
- buildArgs: (path) => ({ path, include: ["java"] }),
43
- },
44
- // Dependency analysis
45
- {
46
- keywords: ["의존성", "dependency", "dependencies", "취약점"],
47
- tool: "dependency_check",
48
- buildArgs: (path) => ({ path }),
49
- },
50
- // Complexity
51
- {
52
- keywords: ["복잡도", "complexity"],
53
- tool: "analyze_all",
54
- buildArgs: (path) => ({ path }),
55
- },
56
- // Python
57
- {
58
- keywords: ["python", "파이썬", ".py"],
59
- tool: "analyze_all",
60
- buildArgs: (path) => ({ path, include: ["py"] }),
61
- },
62
- // Frontend
63
- {
64
- keywords: ["react", "리액트", "vue", "뷰", "프론트엔드", "frontend"],
65
- tool: "analyze_all",
66
- buildArgs: (path) => ({ path, include: ["js", "ts", "jsx", "tsx", "vue"] }),
67
- },
68
- // CSS
69
- {
70
- keywords: ["css", "scss", "less", "스타일"],
71
- tool: "analyze_all",
72
- buildArgs: (path) => ({ path, include: ["css"] }),
73
- },
74
- // HTML
75
- {
76
- keywords: ["html", "jsp", "접근성", "a11y", "seo"],
77
- tool: "analyze_all",
78
- buildArgs: (path) => ({ path, include: ["html"] }),
79
- },
80
- // SQL / MyBatis
81
- {
82
- keywords: ["sql", "mybatis", "마이바티스", "쿼리"],
83
- tool: "analyze_all",
84
- buildArgs: (path) => ({ path, include: ["java", "xml"] }),
85
- },
86
- // Broad analysis (catch-all, must be last)
87
- {
88
- keywords: ["전체분석", "전체 분석", "분석해", "코드품질", "코드 품질", "analyze", "분석", "검사", "check"],
89
- tool: "analyze_all",
90
- buildArgs: (path) => ({ path }),
91
- },
92
- ];
93
- // File extension → single-file tool mapping
94
- const FILE_TOOL_MAP = {
95
- ".java": "java_analyze",
96
- ".js": "ast_analyze",
97
- ".ts": "ast_analyze",
98
- ".jsx": "react_check",
99
- ".tsx": "react_check",
100
- ".vue": "vue_check",
101
- ".py": "python_check",
102
- ".css": "css_check",
103
- ".scss": "css_check",
104
- ".less": "css_check",
105
- ".html": "html_check",
106
- ".htm": "html_check",
107
- ".jsp": "html_check",
108
- };
109
- /**
110
- * Extract filesystem paths from user message.
111
- * Handles quoted paths (with spaces), simple paths, and greedy path expansion.
112
- */
113
- function extractPaths(message) {
114
- const paths = [];
115
- // 1. Quoted paths: '...' or "..."
116
- const quotedMatches = message.match(/['"]([/\\][^'"]+)['"]/g);
117
- if (quotedMatches) {
118
- for (const m of quotedMatches) {
119
- paths.push(m.slice(1, -1)); // strip quotes
120
- }
121
- }
122
- // 2. Simple paths (no spaces) - Unix & Windows
123
- const unixMatches = message.match(/(?:^|\s)(\/[^\s,;:'"]+)/g);
124
- if (unixMatches) {
125
- for (const m of unixMatches) {
126
- paths.push(m.trim());
127
- }
128
- }
129
- const winMatches = message.match(/(?:^|\s)([A-Z]:\\[^\s,;:'"]+)/gi);
130
- if (winMatches) {
131
- for (const m of winMatches) {
132
- paths.push(m.trim());
133
- }
134
- }
135
- // 3. Greedy path expansion: if simple match doesn't exist,
136
- // try extending with subsequent words until path is valid
137
- if (paths.length === 0 || !paths.some((p) => { try {
138
- return fs.existsSync(p);
139
- }
140
- catch {
141
- return false;
142
- } })) {
143
- const words = message.split(/\s+/);
144
- for (let i = 0; i < words.length; i++) {
145
- if (words[i].startsWith("/") || /^[A-Z]:\\/i.test(words[i])) {
146
- // Found a path start, try extending
147
- let candidate = words[i];
148
- let bestPath = "";
149
- // Check initial segment
150
- try {
151
- if (fs.existsSync(candidate))
152
- bestPath = candidate;
153
- }
154
- catch { /* */ }
155
- // Extend with subsequent words
156
- for (let j = i + 1; j < words.length; j++) {
157
- const extended = candidate + " " + words[j];
158
- try {
159
- if (fs.existsSync(extended)) {
160
- bestPath = extended;
161
- candidate = extended;
162
- }
163
- else {
164
- // No more valid extensions - stop
165
- break;
166
- }
167
- }
168
- catch {
169
- break;
170
- }
171
- }
172
- if (bestPath) {
173
- paths.push(bestPath);
174
- }
175
- }
176
- }
177
- }
178
- // Filter to actually existing paths, deduplicate
179
- const seen = new Set();
180
- return paths.filter((p) => {
181
- if (seen.has(p))
182
- return false;
183
- seen.add(p);
184
- try {
185
- return fs.existsSync(p);
186
- }
187
- catch {
188
- return false;
189
- }
190
- });
191
- }
192
- /**
193
- * Determine if a path is a single file (not a directory).
194
- */
195
- function isSingleFile(p) {
196
- try {
197
- return fs.statSync(p).isFile();
198
- }
199
- catch {
200
- return false;
201
- }
202
- }
203
- /**
204
- * Resolve the correct tool for a single file based on extension.
205
- */
206
- function resolveFileAnalysisTool(filepath) {
207
- const ext = filepath.substring(filepath.lastIndexOf(".")).toLowerCase();
208
- const toolName = FILE_TOOL_MAP[ext];
209
- if (!toolName)
210
- return null;
211
- // Some tools use 'filepath', others use 'path'
212
- const argKey = ["python_check", "css_check", "html_check"].includes(toolName) ? "path" : "filepath";
213
- return { tool: toolName, args: { [argKey]: filepath } };
214
- }
215
- /**
216
- * Detect user intent from the message and automatically execute the appropriate tool.
217
- * Returns IntentResult with handled=true if a tool was executed, false otherwise.
218
- */
219
- async function detectAndExecuteIntent(userMessage, onEvent) {
220
- const msg = userMessage.toLowerCase();
221
- const paths = extractPaths(userMessage);
222
- // No path found → can't auto-route
223
- if (paths.length === 0) {
224
- return { handled: false };
225
- }
226
- const targetPath = paths[0];
227
- // Check if path is a single file
228
- if (isSingleFile(targetPath)) {
229
- const fileInfo = resolveFileAnalysisTool(targetPath);
230
- if (fileInfo) {
231
- return await executeIntentTool(fileInfo.tool, fileInfo.args, onEvent);
232
- }
233
- // Unknown file type → fall back to LLM
234
- return { handled: false };
235
- }
236
- // Path is a directory → match intent patterns
237
- for (const pattern of INTENT_PATTERNS) {
238
- // Skip the single-file marker for directories
239
- if (pattern.tool === "_single_file")
240
- continue;
241
- if (pattern.keywords.some((kw) => msg.includes(kw))) {
242
- const args = pattern.buildArgs(targetPath, userMessage);
243
- return await executeIntentTool(pattern.tool, args, onEvent);
244
- }
245
- }
246
- // Has a directory path but no matching keyword → default to analyze_all
247
- // (user likely wants some kind of analysis if they provided a path)
248
- const hasAnalysisHint = /분석|검사|확인|체크|check|analyze|review|scan|report/i.test(msg);
249
- if (hasAnalysisHint) {
250
- return await executeIntentTool("analyze_all", { path: targetPath }, onEvent);
251
- }
252
- return { handled: false };
253
- }
254
- /**
255
- * Execute a tool by name and return an IntentResult with the summary prompt.
256
- */
257
- async function executeIntentTool(toolName, toolArgs, onEvent) {
258
- const toolCall = {
259
- id: `intent_${Date.now()}_${Math.random().toString(36).slice(2)}`,
260
- name: toolName,
261
- arguments: toolArgs,
262
- };
263
- // Emit tool_use start event
264
- onEvent?.({
265
- type: "tool_use",
266
- tool: toolName,
267
- status: "start",
268
- args: toolArgs,
269
- });
270
- const result = await executeTool(toolCall);
271
- // Emit tool_result event
272
- onEvent?.({
273
- type: "tool_result",
274
- tool: toolName,
275
- status: result.success ? "complete" : "error",
276
- result,
277
- });
278
- if (!result.success) {
279
- return {
280
- handled: true,
281
- toolName,
282
- toolArgs,
283
- toolResult: result,
284
- summaryPrompt: `도구 "${toolName}" 실행 중 오류가 발생했습니다: ${result.error}\n사용자에게 오류 내용을 설명해주세요.`,
285
- };
286
- }
287
- // Compress result to fit in context window
288
- const compressed = compressAnalysisResult(result.content);
289
- return {
290
- handled: true,
291
- toolName,
292
- toolArgs,
293
- toolResult: result,
294
- summaryPrompt: `아래는 "${toolName}" 도구의 실행 결과입니다. 사용자에게 한국어로 핵심 내용을 요약해주세요.\n\n${compressed}`,
295
- };
296
- }
297
- /**
298
- * Compress analysis result JSON to fit within LLM context window.
299
- * Extracts only key metrics, removing verbose raw data.
300
- */
301
- function compressAnalysisResult(resultContent, maxChars = 2000) {
302
- try {
303
- const parsed = JSON.parse(resultContent);
304
- // analyze_all result
305
- if (parsed.path && parsed.fileStats) {
306
- const compact = {
307
- path: parsed.path,
308
- totalFiles: parsed.totalFiles,
309
- fileStats: parsed.fileStats,
310
- analysesRun: parsed.analysesRun,
311
- successful: parsed.successful,
312
- failed: parsed.failed,
313
- };
314
- // Extract issue summaries (compact)
315
- if (parsed.issuesSummary?.length > 0) {
316
- compact.issues = parsed.issuesSummary.map((is) => ({
317
- tool: is.tool,
318
- issues: is.issues.slice(0, 5),
319
- }));
320
- }
321
- // Extract per-tool summaries (key metrics only)
322
- if (parsed.details?.length > 0) {
323
- compact.analyses = parsed.details.map((d) => {
324
- const s = d.summary;
325
- const brief = { tool: d.tool };
326
- // Extract numeric/small fields only
327
- for (const [k, v] of Object.entries(s)) {
328
- if (typeof v === "number" || typeof v === "boolean") {
329
- brief[k] = v;
330
- }
331
- else if (typeof v === "string" && v.length < 100) {
332
- brief[k] = v;
333
- }
334
- // Skip arrays/objects (raw data) to save space
335
- }
336
- // Include issues from samples (java_analyze etc.)
337
- if (Array.isArray(s.samples)) {
338
- const allIssues = [];
339
- for (const sample of s.samples) {
340
- if (Array.isArray(sample.result?.issues)) {
341
- allIssues.push(...sample.result.issues.slice(0, 3));
342
- }
343
- }
344
- if (allIssues.length > 0) {
345
- brief.issues = allIssues.slice(0, 10);
346
- }
347
- }
348
- return brief;
349
- });
350
- }
351
- if (parsed.errors?.length > 0) {
352
- compact.errors = parsed.errors;
353
- }
354
- const result = JSON.stringify(compact, null, 1);
355
- return result.length > maxChars ? result.slice(0, maxChars) + "..." : result;
356
- }
357
- // java_analyze or other single-file results
358
- if (parsed.file || parsed.filepath || parsed.classes || parsed.functions) {
359
- const result = JSON.stringify(parsed, null, 1);
360
- return result.length > maxChars ? result.slice(0, maxChars) + "..." : result;
361
- }
362
- // Generic: just truncate
363
- const result = JSON.stringify(parsed, null, 1);
364
- return result.length > maxChars ? result.slice(0, maxChars) + "..." : result;
365
- }
366
- catch {
367
- // Not valid JSON, return truncated raw text
368
- return resultContent.length > maxChars ? resultContent.slice(0, maxChars) + "..." : resultContent;
369
- }
370
- }
371
46
  // ─── Main processing functions ───
372
47
  export async function processMessage(userMessage, history, client, config, onEvent, contextSummary) {
373
48
  // Try intent router first
@@ -376,7 +51,7 @@ export async function processMessage(userMessage, history, client, config, onEve
376
51
  // Tool already executed → ask LLM to summarize only (no tools = VRAM savings)
377
52
  onEvent?.({ type: "thinking" });
378
53
  const summaryMessages = [
379
- { role: "system", content: BASE_SYSTEM_PROMPT },
54
+ { role: "system", content: SUMMARY_SYSTEM_PROMPT },
380
55
  { role: "user", content: intent.summaryPrompt },
381
56
  ];
382
57
  const response = await client.chat(summaryMessages); // No tools!
@@ -400,17 +75,51 @@ export async function processMessage(userMessage, history, client, config, onEve
400
75
  const toolCallResults = [];
401
76
  let finalContent = "";
402
77
  let iterations = 0;
403
- const maxIterations = 10;
78
+ const maxIterations = 30;
79
+ const recentSignatures = [];
404
80
  while (iterations < maxIterations) {
405
81
  iterations++;
406
82
  onEvent?.({ type: "thinking" });
407
83
  const response = await client.chat(messages, tools);
408
84
  messages.push(response);
409
- // If no tool calls, we're done
85
+ // If no tool calls, we're done — but check if final answer is empty
410
86
  if (!response.toolCalls?.length) {
411
87
  finalContent = response.content;
88
+ // 도구를 실행했는데 최종 응답이 비어있으면 → LLM에게 요약 강제 요청
89
+ if (!finalContent && toolCallResults.length > 0) {
90
+ messages.push({
91
+ role: "user",
92
+ content: "도구 실행이 완료되었습니다. 결과를 요약하고, 생성된 파일이 있다면 경로를 알려주세요.",
93
+ });
94
+ try {
95
+ const summaryResponse = await client.chat(messages); // No tools
96
+ finalContent = summaryResponse.content;
97
+ }
98
+ catch {
99
+ finalContent = toolCallResults
100
+ .map((tc) => `- ${tc.tool}(${Object.values(tc.args)[0] || ""}): ${tc.result.success ? "✓" : "✗"}`)
101
+ .join("\n");
102
+ finalContent = `수행된 작업:\n${finalContent}`;
103
+ }
104
+ onEvent?.({ type: "content", content: finalContent });
105
+ }
412
106
  break;
413
107
  }
108
+ // Detect true loops: same tool + same args 3+ times consecutively
109
+ for (const tc of response.toolCalls) {
110
+ recentSignatures.push(toolSignature(tc));
111
+ }
112
+ if (recentSignatures.length >= 3) {
113
+ const last3 = recentSignatures.slice(-3);
114
+ if (last3.every((s) => s === last3[0])) {
115
+ const toolSummary = toolCallResults
116
+ .map((tc) => `- ${tc.tool}(${Object.values(tc.args)[0] || ""}): ${tc.result.success ? "✓" : "✗"}`)
117
+ .join("\n");
118
+ finalContent = `동일한 작업이 반복되어 중단합니다.\n\n수행된 작업:\n${toolSummary}`;
119
+ onEvent?.({ type: "content", content: finalContent });
120
+ break;
121
+ }
122
+ }
414
123
  // Process tool calls
415
124
  for (const toolCall of response.toolCalls) {
416
125
  onEvent?.({
@@ -431,18 +140,45 @@ export async function processMessage(userMessage, history, client, config, onEve
431
140
  args: toolCall.arguments,
432
141
  result,
433
142
  });
434
- // Add tool result to messages
143
+ // Add tool result to messages (compressed to avoid context overflow)
144
+ const toolContent = result.success
145
+ ? compressAnalysisResult(result.content, 8000)
146
+ : `Error: ${result.error}`;
435
147
  messages.push({
436
148
  role: "tool",
437
- content: result.success ? result.content : `Error: ${result.error}`,
149
+ content: toolContent,
438
150
  toolCallId: toolCall.id,
439
151
  });
440
152
  }
441
153
  // Continue the conversation with tool results
442
154
  onEvent?.({ type: "content", content: response.content });
443
155
  }
156
+ // 도구는 실행됐지만 최종 content가 비어있는 경우 → 요약 표시
157
+ if (!finalContent && toolCallResults.length > 0 && iterations < maxIterations) {
158
+ finalContent = toolCallResults
159
+ .map((tc) => `- ${tc.tool}(${Object.values(tc.args)[0] || ""}): ${tc.result.success ? "✓" : "✗"}`)
160
+ .join("\n");
161
+ finalContent = `수행된 작업:\n${finalContent}`;
162
+ onEvent?.({ type: "content", content: finalContent });
163
+ }
444
164
  if (iterations >= maxIterations) {
445
- onEvent?.({ type: "error", error: "Maximum iterations reached" });
165
+ // Force LLM to generate final answer without tools
166
+ messages.push({
167
+ role: "user",
168
+ content: "지금까지 수행된 작업 결과를 바탕으로 최종 답변을 작성해주세요. 더 이상 도구를 호출하지 마세요.",
169
+ });
170
+ try {
171
+ const finalResponse = await client.chat(messages); // No tools!
172
+ finalContent = finalResponse.content;
173
+ onEvent?.({ type: "content", content: finalContent });
174
+ }
175
+ catch {
176
+ const toolSummary = toolCallResults
177
+ .map((tc) => `- ${tc.tool}(${Object.values(tc.args)[0] || ""}): ${tc.result.success ? "✓" : "✗"}`)
178
+ .join("\n");
179
+ finalContent = `작업이 최대 반복 횟수에 도달했습니다.\n\n수행된 작업:\n${toolSummary}`;
180
+ onEvent?.({ type: "content", content: finalContent });
181
+ }
446
182
  }
447
183
  onEvent?.({ type: "done" });
448
184
  return {
@@ -451,26 +187,15 @@ export async function processMessage(userMessage, history, client, config, onEve
451
187
  };
452
188
  }
453
189
  export async function* streamProcessMessage(userMessage, history, client, config, abortSignal, contextSummary) {
454
- // Try intent router first
190
+ // Try intent router first — collect events from pipeline for later emission
191
+ const collectedEvents = [];
455
192
  const intent = await detectAndExecuteIntent(userMessage, (event) => {
456
- // Events are yielded by the caller, we collect them via callback
457
- // But generators can't yield from callbacks, so we handle this differently
193
+ collectedEvents.push(event);
458
194
  });
459
195
  if (intent.handled) {
460
- // Emit the tool events that happened during intent detection
461
- if (intent.toolName) {
462
- yield {
463
- type: "tool_use",
464
- tool: intent.toolName,
465
- status: "start",
466
- args: intent.toolArgs,
467
- };
468
- yield {
469
- type: "tool_result",
470
- tool: intent.toolName,
471
- status: intent.toolResult?.success ? "complete" : "error",
472
- result: intent.toolResult,
473
- };
196
+ // Emit all events that happened during intent detection (pipeline steps)
197
+ for (const event of collectedEvents) {
198
+ yield event;
474
199
  }
475
200
  if (intent.summaryPrompt) {
476
201
  if (abortSignal?.aborted) {
@@ -480,7 +205,7 @@ export async function* streamProcessMessage(userMessage, history, client, config
480
205
  yield { type: "thinking" };
481
206
  // Stream the LLM summary (no tools = streaming mode in ollama client)
482
207
  const summaryMessages = [
483
- { role: "system", content: BASE_SYSTEM_PROMPT },
208
+ { role: "system", content: SUMMARY_SYSTEM_PROMPT },
484
209
  { role: "user", content: intent.summaryPrompt },
485
210
  ];
486
211
  for await (const event of client.streamChat(summaryMessages, undefined, abortSignal)) {
@@ -509,7 +234,10 @@ export async function* streamProcessMessage(userMessage, history, client, config
509
234
  { role: "user", content: userMessage },
510
235
  ];
511
236
  let iterations = 0;
512
- const maxIterations = 10;
237
+ const maxIterations = 30;
238
+ const recentSignatures = [];
239
+ const completedToolCalls = [];
240
+ let contentYielded = false;
513
241
  while (iterations < maxIterations) {
514
242
  // Check if aborted
515
243
  if (abortSignal?.aborted) {
@@ -528,7 +256,6 @@ export async function* streamProcessMessage(userMessage, history, client, config
528
256
  }
529
257
  if (event.type === "content" && event.content) {
530
258
  fullContent += event.content;
531
- // Don't yield content yet - wait to see if there are tool calls
532
259
  }
533
260
  else if (event.type === "tool_call" && event.toolCall) {
534
261
  pendingToolCalls.push(event.toolCall);
@@ -538,19 +265,67 @@ export async function* streamProcessMessage(userMessage, history, client, config
538
265
  return;
539
266
  }
540
267
  }
541
- // Only yield content if NO tool calls (avoid hallucinated pre-tool text)
542
- if (pendingToolCalls.length === 0 && fullContent) {
268
+ // content 있으면 항상 yield (tool call 유무 무관)
269
+ if (fullContent) {
543
270
  yield { type: "content", content: fullContent };
271
+ contentYielded = true;
544
272
  }
545
- else if (pendingToolCalls.length > 0) {
546
- // Clear content when tool calls exist
547
- fullContent = "";
548
- }
549
- messages.push({ role: "assistant", content: fullContent, toolCalls: pendingToolCalls.length > 0 ? pendingToolCalls : undefined });
550
- // If no tool calls, we're done
273
+ // message history에 content 항상 보존
274
+ messages.push({
275
+ role: "assistant",
276
+ content: fullContent,
277
+ toolCalls: pendingToolCalls.length > 0 ? pendingToolCalls : undefined,
278
+ });
279
+ // If no tool calls, we're done — but check if final answer is empty
551
280
  if (pendingToolCalls.length === 0) {
281
+ // 도구를 실행했는데 최종 응답이 비어있으면 → LLM에게 요약 강제 요청
282
+ if (!fullContent && completedToolCalls.length > 0) {
283
+ let summaryYielded = false;
284
+ messages.push({
285
+ role: "user",
286
+ content: "도구 실행이 완료되었습니다. 결과를 요약하고, 생성된 파일이 있다면 경로를 알려주세요.",
287
+ });
288
+ try {
289
+ for await (const event of client.streamChat(messages, undefined, abortSignal)) {
290
+ if (abortSignal?.aborted)
291
+ break;
292
+ if (event.type === "content" && event.content) {
293
+ yield { type: "content", content: event.content };
294
+ summaryYielded = true;
295
+ contentYielded = true;
296
+ }
297
+ }
298
+ }
299
+ catch {
300
+ // API 에러 시 fallback
301
+ }
302
+ if (!summaryYielded) {
303
+ const toolSummary = completedToolCalls
304
+ .map((tc) => `- ${tc.tool}(${Object.values(tc.args)[0] || ""}): ${tc.success ? "✓" : "✗"}`)
305
+ .join("\n");
306
+ yield { type: "content", content: `수행된 작업:\n${toolSummary}` };
307
+ contentYielded = true;
308
+ }
309
+ }
552
310
  break;
553
311
  }
312
+ // Detect true loops: same tool + same args 3+ times consecutively
313
+ for (const tc of pendingToolCalls) {
314
+ recentSignatures.push(toolSignature(tc));
315
+ }
316
+ if (recentSignatures.length >= 3) {
317
+ const last3 = recentSignatures.slice(-3);
318
+ if (last3.every((s) => s === last3[0])) {
319
+ const toolSummary = completedToolCalls
320
+ .map((tc) => `- ${tc.tool}(${Object.values(tc.args)[0] || ""}): ${tc.success ? "✓" : "✗"}`)
321
+ .join("\n");
322
+ yield {
323
+ type: "content",
324
+ content: `\n\n동일한 작업이 반복되어 중단합니다.\n\n수행된 작업:\n${toolSummary}`,
325
+ };
326
+ break;
327
+ }
328
+ }
554
329
  // Process tool calls
555
330
  for (const toolCall of pendingToolCalls) {
556
331
  // Check if aborted before each tool call
@@ -576,13 +351,57 @@ export async function* streamProcessMessage(userMessage, history, client, config
576
351
  status: result.success ? "complete" : "error",
577
352
  result,
578
353
  };
354
+ completedToolCalls.push({
355
+ tool: toolCall.name,
356
+ args: toolCall.arguments,
357
+ success: result.success,
358
+ });
359
+ // Compress tool result to avoid context overflow (especially for Anthropic 200k limit)
360
+ const toolContent = result.success
361
+ ? compressAnalysisResult(result.content, 8000)
362
+ : `Error: ${result.error}`;
579
363
  messages.push({
580
364
  role: "tool",
581
- content: result.success ? result.content : `Error: ${result.error}`,
365
+ content: toolContent,
582
366
  toolCallId: toolCall.id,
583
367
  });
584
368
  }
585
369
  }
370
+ // 도구는 실행됐지만 content가 한 번도 yield 안 된 경우 → 요약 표시
371
+ if (!contentYielded && completedToolCalls.length > 0 && iterations < maxIterations) {
372
+ const toolSummary = completedToolCalls
373
+ .map((tc) => `- ${tc.tool}(${Object.values(tc.args)[0] || ""}): ${tc.success ? "✓" : "✗"}`)
374
+ .join("\n");
375
+ yield { type: "content", content: `수행된 작업:\n${toolSummary}` };
376
+ }
377
+ // If maxIterations reached, force LLM to generate final answer
378
+ if (iterations >= maxIterations) {
379
+ // Add a nudge message asking LLM to summarize
380
+ messages.push({
381
+ role: "user",
382
+ content: "지금까지 수행된 작업 결과를 바탕으로 최종 답변을 작성해주세요. 더 이상 도구를 호출하지 마세요.",
383
+ });
384
+ try {
385
+ // One final LLM call WITHOUT tools to force a text response
386
+ for await (const event of client.streamChat(messages, undefined, abortSignal)) {
387
+ if (abortSignal?.aborted)
388
+ break;
389
+ if (event.type === "content" && event.content) {
390
+ yield { type: "content", content: event.content };
391
+ }
392
+ }
393
+ }
394
+ catch {
395
+ // Fallback: show tool summary if final LLM call fails
396
+ const toolSummary = completedToolCalls
397
+ .map((tc) => `- ${tc.tool}(${Object.values(tc.args)[0] || ""}): ${tc.success ? "✓" : "✗"}`)
398
+ .join("\n");
399
+ yield {
400
+ type: "content",
401
+ content: `\n\n작업이 최대 반복 횟수에 도달했습니다.\n\n수행된 작업:\n${toolSummary}`,
402
+ };
403
+ }
404
+ }
586
405
  yield { type: "done" };
587
406
  }
588
407
  //# sourceMappingURL=agent.js.map