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.
- package/README.md +203 -1
- package/data/2026-03-04_20-54.json +181 -0
- package/data/2026-03-04_20-56.json +181 -0
- package/data/apex-rulesets/egov.yaml +469 -0
- package/data/apex-rulesets/modernize.yaml +687 -0
- package/data/apex-rulesets/quality.yaml +1677 -0
- package/data/apex-rulesets/rule-schema.yaml +587 -0
- package/data/apex-rulesets/secure.yaml +1688 -0
- package/data/apex-rulesets/spring.yaml +455 -0
- package/data/apex-rulesets/sql-format.yaml +99 -0
- package/data/apex-rulesets/sql-oracle.yaml +281 -0
- package/data/apex-rulesets/sql.yaml +1660 -0
- package/dist/cli/headless.d.ts.map +1 -1
- package/dist/cli/headless.js +32 -10
- package/dist/cli/headless.js.map +1 -1
- package/dist/cli/index.js +31 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/core/agent.d.ts +3 -3
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +203 -384
- package/dist/core/agent.js.map +1 -1
- package/dist/core/commands.d.ts +2 -1
- package/dist/core/commands.d.ts.map +1 -1
- package/dist/core/commands.js +61 -9
- package/dist/core/commands.js.map +1 -1
- package/dist/core/config.d.ts +14 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +41 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/conversation.d.ts +2 -2
- package/dist/core/conversation.d.ts.map +1 -1
- package/dist/core/conversation.js.map +1 -1
- package/dist/core/intentRouter.d.ts +43 -0
- package/dist/core/intentRouter.d.ts.map +1 -0
- package/dist/core/intentRouter.js +804 -0
- package/dist/core/intentRouter.js.map +1 -0
- package/dist/core/llm/anthropic.d.ts +24 -0
- package/dist/core/llm/anthropic.d.ts.map +1 -0
- package/dist/core/llm/anthropic.js +226 -0
- package/dist/core/llm/anthropic.js.map +1 -0
- package/dist/core/llm/ollama.d.ts +5 -14
- package/dist/core/llm/ollama.d.ts.map +1 -1
- package/dist/core/llm/ollama.js +3 -0
- package/dist/core/llm/ollama.js.map +1 -1
- package/dist/core/llm/types.d.ts +22 -0
- package/dist/core/llm/types.d.ts.map +1 -0
- package/dist/core/llm/types.js +2 -0
- package/dist/core/llm/types.js.map +1 -0
- package/dist/core/mcp/client.d.ts +6 -0
- package/dist/core/mcp/client.d.ts.map +1 -1
- package/dist/core/mcp/client.js +16 -0
- package/dist/core/mcp/client.js.map +1 -1
- package/dist/core/mcp/init.d.ts +12 -0
- package/dist/core/mcp/init.d.ts.map +1 -0
- package/dist/core/mcp/init.js +55 -0
- package/dist/core/mcp/init.js.map +1 -0
- package/dist/core/mcp/logger.d.ts +14 -0
- package/dist/core/mcp/logger.d.ts.map +1 -0
- package/dist/core/mcp/logger.js +50 -0
- package/dist/core/mcp/logger.js.map +1 -0
- package/dist/core/tools/analyzePatterns.d.ts +3 -0
- package/dist/core/tools/analyzePatterns.d.ts.map +1 -0
- package/dist/core/tools/analyzePatterns.js +293 -0
- package/dist/core/tools/analyzePatterns.js.map +1 -0
- package/dist/core/tools/apexPaths.d.ts +14 -0
- package/dist/core/tools/apexPaths.d.ts.map +1 -0
- package/dist/core/tools/apexPaths.js +54 -0
- package/dist/core/tools/apexPaths.js.map +1 -0
- package/dist/core/tools/apexUtils.d.ts +36 -0
- package/dist/core/tools/apexUtils.d.ts.map +1 -0
- package/dist/core/tools/apexUtils.js +83 -0
- package/dist/core/tools/apexUtils.js.map +1 -0
- package/dist/core/tools/explainIssue.d.ts +3 -0
- package/dist/core/tools/explainIssue.d.ts.map +1 -0
- package/dist/core/tools/explainIssue.js +181 -0
- package/dist/core/tools/explainIssue.js.map +1 -0
- package/dist/core/tools/fixGen.d.ts +3 -0
- package/dist/core/tools/fixGen.d.ts.map +1 -0
- package/dist/core/tools/fixGen.js +338 -0
- package/dist/core/tools/fixGen.js.map +1 -0
- package/dist/core/tools/generateImprovements.d.ts +21 -0
- package/dist/core/tools/generateImprovements.d.ts.map +1 -0
- package/dist/core/tools/generateImprovements.js +602 -0
- package/dist/core/tools/generateImprovements.js.map +1 -0
- package/dist/core/tools/generateReport.d.ts +3 -0
- package/dist/core/tools/generateReport.d.ts.map +1 -0
- package/dist/core/tools/generateReport.js +315 -0
- package/dist/core/tools/generateReport.js.map +1 -0
- package/dist/core/tools/index.d.ts +7 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +62 -23
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/recommendProfile.d.ts +3 -0
- package/dist/core/tools/recommendProfile.d.ts.map +1 -0
- package/dist/core/tools/recommendProfile.js +334 -0
- package/dist/core/tools/recommendProfile.js.map +1 -0
- package/dist/core/tools/ruleGen.d.ts +3 -0
- package/dist/core/tools/ruleGen.d.ts.map +1 -0
- package/dist/core/tools/ruleGen.js +1103 -0
- package/dist/core/tools/ruleGen.js.map +1 -0
- package/dist/core/tools/standards.d.ts.map +1 -1
- package/dist/core/tools/standards.js +7 -3
- package/dist/core/tools/standards.js.map +1 -1
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +86 -35
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/components/InputBox.d.ts +1 -3
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +146 -5
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/MessageList.d.ts +3 -1
- package/dist/ui/components/MessageList.d.ts.map +1 -1
- package/dist/ui/components/MessageList.js +13 -7
- package/dist/ui/components/MessageList.js.map +1 -1
- package/dist/ui/components/StatusBar.d.ts +1 -1
- package/dist/ui/components/StatusBar.d.ts.map +1 -1
- package/dist/ui/components/StatusBar.js +3 -2
- package/dist/ui/components/StatusBar.js.map +1 -1
- package/dist/ui/components/ToolStatus.d.ts +3 -1
- package/dist/ui/components/ToolStatus.d.ts.map +1 -1
- package/dist/ui/components/ToolStatus.js +19 -4
- package/dist/ui/components/ToolStatus.js.map +1 -1
- package/package.json +7 -1
- package/demo.gif +0 -0
- package/demo.tape +0 -53
- package/screenshot.png +0 -0
- package/src/cli/banner.ts +0 -38
- package/src/cli/headless.ts +0 -63
- package/src/cli/index.ts +0 -57
- package/src/core/agent.ts +0 -711
- package/src/core/commands.ts +0 -118
- package/src/core/config.ts +0 -98
- package/src/core/conversation.ts +0 -235
- package/src/core/llm/ollama.ts +0 -351
- package/src/core/mcp/client.ts +0 -143
- package/src/core/tools/analyzeAll.ts +0 -482
- package/src/core/tools/ast.ts +0 -826
- package/src/core/tools/builtIn.ts +0 -221
- package/src/core/tools/cache.ts +0 -570
- package/src/core/tools/cssAnalysis.ts +0 -324
- package/src/core/tools/dependencyAnalysis.ts +0 -363
- package/src/core/tools/embeddings.ts +0 -746
- package/src/core/tools/frontendAst.ts +0 -802
- package/src/core/tools/htmlAnalysis.ts +0 -466
- package/src/core/tools/index.ts +0 -160
- package/src/core/tools/javaAst.ts +0 -1030
- package/src/core/tools/javaQuality.integration.test.ts +0 -537
- package/src/core/tools/memory.ts +0 -655
- package/src/core/tools/mybatisAnalysis.ts +0 -322
- package/src/core/tools/openapiAnalysis.ts +0 -431
- package/src/core/tools/pythonAnalysis.ts +0 -477
- package/src/core/tools/sqlAnalysis.ts +0 -298
- package/src/core/tools/standards.test.ts +0 -186
- package/src/core/tools/standards.ts +0 -889
- package/src/core/tools/types.ts +0 -38
- package/src/ui/App.tsx +0 -334
- package/src/ui/components/InputBox.tsx +0 -37
- package/src/ui/components/MessageList.tsx +0 -80
- package/src/ui/components/StatusBar.tsx +0 -36
- package/src/ui/components/ToolStatus.tsx +0 -38
- package/tsconfig.json +0 -21
package/dist/core/agent.js
CHANGED
|
@@ -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:
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
461
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
//
|
|
542
|
-
if (
|
|
268
|
+
// content가 있으면 항상 yield (tool call 유무 무관)
|
|
269
|
+
if (fullContent) {
|
|
543
270
|
yield { type: "content", content: fullContent };
|
|
271
|
+
contentYielded = true;
|
|
544
272
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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:
|
|
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
|