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
package/src/core/agent.ts DELETED
@@ -1,711 +0,0 @@
1
- import * as fs from "fs";
2
- import { OllamaClient, ChatMessage } from "./llm/ollama.js";
3
- import { Config } from "./config.js";
4
- import { getAllTools, selectTools, executeTool, ToolCall, ToolResult, Tool } from "./tools/index.js";
5
-
6
- export interface AgentEvent {
7
- type: "thinking" | "content" | "tool_use" | "tool_result" | "done" | "error";
8
- content?: string;
9
- tool?: string;
10
- status?: "start" | "complete" | "error";
11
- args?: Record<string, unknown>;
12
- result?: ToolResult;
13
- error?: string;
14
- }
15
-
16
- export interface AgentResult {
17
- content: string;
18
- toolCalls: Array<{
19
- tool: string;
20
- args: Record<string, unknown>;
21
- result: ToolResult;
22
- }>;
23
- }
24
-
25
- const BASE_SYSTEM_PROMPT = `You are ACTIVO, a code quality analyzer. You MUST call tools to perform tasks.
26
-
27
- ## RULES
28
- 1. Call tool IMMEDIATELY when user requests an action
29
- 2. NEVER fabricate results - only report actual tool output
30
- 3. After tool returns, summarize in user's language (Korean if user speaks Korean)
31
- 4. Use analyze_all for broad code analysis`;
32
-
33
- // Build system prompt with optional context
34
- function buildSystemPrompt(contextSummary?: string): string {
35
- if (!contextSummary) {
36
- return BASE_SYSTEM_PROMPT;
37
- }
38
-
39
- return `${BASE_SYSTEM_PROMPT}
40
-
41
- ## 이전 대화 컨텍스트
42
-
43
- ${contextSummary}
44
-
45
- ---
46
- 위 내용은 이전 세션에서의 대화 요약입니다. 필요시 참고하세요.`;
47
- }
48
-
49
- // ─── Intent Router ───
50
-
51
- interface IntentResult {
52
- handled: boolean;
53
- toolName?: string;
54
- toolArgs?: Record<string, unknown>;
55
- toolResult?: ToolResult;
56
- summaryPrompt?: string;
57
- }
58
-
59
- interface IntentPattern {
60
- keywords: string[];
61
- tool: string;
62
- buildArgs: (path: string, message: string) => Record<string, unknown>;
63
- }
64
-
65
- // Intent patterns: keyword groups → tool + args builder
66
- const INTENT_PATTERNS: IntentPattern[] = [
67
- // Single file analysis (must come before directory patterns)
68
- {
69
- keywords: ["분석", "analyze", "검사", "check"],
70
- tool: "_single_file", // special marker - resolved at match time
71
- buildArgs: (path: string) => ({ filepath: path }),
72
- },
73
- // analyze_all with Java filter
74
- {
75
- keywords: ["자바", "java"],
76
- tool: "analyze_all",
77
- buildArgs: (path: string) => ({ path, include: ["java"] }),
78
- },
79
- // Spring patterns
80
- {
81
- keywords: ["spring", "스프링"],
82
- tool: "analyze_all",
83
- buildArgs: (path: string) => ({ path, include: ["java"] }),
84
- },
85
- // Dependency analysis
86
- {
87
- keywords: ["의존성", "dependency", "dependencies", "취약점"],
88
- tool: "dependency_check",
89
- buildArgs: (path: string) => ({ path }),
90
- },
91
- // Complexity
92
- {
93
- keywords: ["복잡도", "complexity"],
94
- tool: "analyze_all",
95
- buildArgs: (path: string) => ({ path }),
96
- },
97
- // Python
98
- {
99
- keywords: ["python", "파이썬", ".py"],
100
- tool: "analyze_all",
101
- buildArgs: (path: string) => ({ path, include: ["py"] }),
102
- },
103
- // Frontend
104
- {
105
- keywords: ["react", "리액트", "vue", "뷰", "프론트엔드", "frontend"],
106
- tool: "analyze_all",
107
- buildArgs: (path: string) => ({ path, include: ["js", "ts", "jsx", "tsx", "vue"] }),
108
- },
109
- // CSS
110
- {
111
- keywords: ["css", "scss", "less", "스타일"],
112
- tool: "analyze_all",
113
- buildArgs: (path: string) => ({ path, include: ["css"] }),
114
- },
115
- // HTML
116
- {
117
- keywords: ["html", "jsp", "접근성", "a11y", "seo"],
118
- tool: "analyze_all",
119
- buildArgs: (path: string) => ({ path, include: ["html"] }),
120
- },
121
- // SQL / MyBatis
122
- {
123
- keywords: ["sql", "mybatis", "마이바티스", "쿼리"],
124
- tool: "analyze_all",
125
- buildArgs: (path: string) => ({ path, include: ["java", "xml"] }),
126
- },
127
- // Broad analysis (catch-all, must be last)
128
- {
129
- keywords: ["전체분석", "전체 분석", "분석해", "코드품질", "코드 품질", "analyze", "분석", "검사", "check"],
130
- tool: "analyze_all",
131
- buildArgs: (path: string) => ({ path }),
132
- },
133
- ];
134
-
135
- // File extension → single-file tool mapping
136
- const FILE_TOOL_MAP: Record<string, string> = {
137
- ".java": "java_analyze",
138
- ".js": "ast_analyze",
139
- ".ts": "ast_analyze",
140
- ".jsx": "react_check",
141
- ".tsx": "react_check",
142
- ".vue": "vue_check",
143
- ".py": "python_check",
144
- ".css": "css_check",
145
- ".scss": "css_check",
146
- ".less": "css_check",
147
- ".html": "html_check",
148
- ".htm": "html_check",
149
- ".jsp": "html_check",
150
- };
151
-
152
- /**
153
- * Extract filesystem paths from user message.
154
- * Handles quoted paths (with spaces), simple paths, and greedy path expansion.
155
- */
156
- function extractPaths(message: string): string[] {
157
- const paths: string[] = [];
158
-
159
- // 1. Quoted paths: '...' or "..."
160
- const quotedMatches = message.match(/['"]([/\\][^'"]+)['"]/g);
161
- if (quotedMatches) {
162
- for (const m of quotedMatches) {
163
- paths.push(m.slice(1, -1)); // strip quotes
164
- }
165
- }
166
-
167
- // 2. Simple paths (no spaces) - Unix & Windows
168
- const unixMatches = message.match(/(?:^|\s)(\/[^\s,;:'"]+)/g);
169
- if (unixMatches) {
170
- for (const m of unixMatches) {
171
- paths.push(m.trim());
172
- }
173
- }
174
- const winMatches = message.match(/(?:^|\s)([A-Z]:\\[^\s,;:'"]+)/gi);
175
- if (winMatches) {
176
- for (const m of winMatches) {
177
- paths.push(m.trim());
178
- }
179
- }
180
-
181
- // 3. Greedy path expansion: if simple match doesn't exist,
182
- // try extending with subsequent words until path is valid
183
- if (paths.length === 0 || !paths.some((p) => { try { return fs.existsSync(p); } catch { return false; } })) {
184
- const words = message.split(/\s+/);
185
- for (let i = 0; i < words.length; i++) {
186
- if (words[i].startsWith("/") || /^[A-Z]:\\/i.test(words[i])) {
187
- // Found a path start, try extending
188
- let candidate = words[i];
189
- let bestPath = "";
190
- // Check initial segment
191
- try { if (fs.existsSync(candidate)) bestPath = candidate; } catch { /* */ }
192
- // Extend with subsequent words
193
- for (let j = i + 1; j < words.length; j++) {
194
- const extended = candidate + " " + words[j];
195
- try {
196
- if (fs.existsSync(extended)) {
197
- bestPath = extended;
198
- candidate = extended;
199
- } else {
200
- // No more valid extensions - stop
201
- break;
202
- }
203
- } catch {
204
- break;
205
- }
206
- }
207
- if (bestPath) {
208
- paths.push(bestPath);
209
- }
210
- }
211
- }
212
- }
213
-
214
- // Filter to actually existing paths, deduplicate
215
- const seen = new Set<string>();
216
- return paths.filter((p) => {
217
- if (seen.has(p)) return false;
218
- seen.add(p);
219
- try {
220
- return fs.existsSync(p);
221
- } catch {
222
- return false;
223
- }
224
- });
225
- }
226
-
227
- /**
228
- * Determine if a path is a single file (not a directory).
229
- */
230
- function isSingleFile(p: string): boolean {
231
- try {
232
- return fs.statSync(p).isFile();
233
- } catch {
234
- return false;
235
- }
236
- }
237
-
238
- /**
239
- * Resolve the correct tool for a single file based on extension.
240
- */
241
- function resolveFileAnalysisTool(filepath: string): { tool: string; args: Record<string, unknown> } | null {
242
- const ext = filepath.substring(filepath.lastIndexOf(".")).toLowerCase();
243
- const toolName = FILE_TOOL_MAP[ext];
244
- if (!toolName) return null;
245
-
246
- // Some tools use 'filepath', others use 'path'
247
- const argKey = ["python_check", "css_check", "html_check"].includes(toolName) ? "path" : "filepath";
248
- return { tool: toolName, args: { [argKey]: filepath } };
249
- }
250
-
251
- /**
252
- * Detect user intent from the message and automatically execute the appropriate tool.
253
- * Returns IntentResult with handled=true if a tool was executed, false otherwise.
254
- */
255
- async function detectAndExecuteIntent(
256
- userMessage: string,
257
- onEvent?: (event: AgentEvent) => void
258
- ): Promise<IntentResult> {
259
- const msg = userMessage.toLowerCase();
260
- const paths = extractPaths(userMessage);
261
-
262
- // No path found → can't auto-route
263
- if (paths.length === 0) {
264
- return { handled: false };
265
- }
266
-
267
- const targetPath = paths[0];
268
-
269
- // Check if path is a single file
270
- if (isSingleFile(targetPath)) {
271
- const fileInfo = resolveFileAnalysisTool(targetPath);
272
- if (fileInfo) {
273
- return await executeIntentTool(fileInfo.tool, fileInfo.args, onEvent);
274
- }
275
- // Unknown file type → fall back to LLM
276
- return { handled: false };
277
- }
278
-
279
- // Path is a directory → match intent patterns
280
- for (const pattern of INTENT_PATTERNS) {
281
- // Skip the single-file marker for directories
282
- if (pattern.tool === "_single_file") continue;
283
-
284
- if (pattern.keywords.some((kw) => msg.includes(kw))) {
285
- const args = pattern.buildArgs(targetPath, userMessage);
286
- return await executeIntentTool(pattern.tool, args, onEvent);
287
- }
288
- }
289
-
290
- // Has a directory path but no matching keyword → default to analyze_all
291
- // (user likely wants some kind of analysis if they provided a path)
292
- const hasAnalysisHint = /분석|검사|확인|체크|check|analyze|review|scan|report/i.test(msg);
293
- if (hasAnalysisHint) {
294
- return await executeIntentTool("analyze_all", { path: targetPath }, onEvent);
295
- }
296
-
297
- return { handled: false };
298
- }
299
-
300
- /**
301
- * Execute a tool by name and return an IntentResult with the summary prompt.
302
- */
303
- async function executeIntentTool(
304
- toolName: string,
305
- toolArgs: Record<string, unknown>,
306
- onEvent?: (event: AgentEvent) => void
307
- ): Promise<IntentResult> {
308
- const toolCall: ToolCall = {
309
- id: `intent_${Date.now()}_${Math.random().toString(36).slice(2)}`,
310
- name: toolName,
311
- arguments: toolArgs,
312
- };
313
-
314
- // Emit tool_use start event
315
- onEvent?.({
316
- type: "tool_use",
317
- tool: toolName,
318
- status: "start",
319
- args: toolArgs,
320
- });
321
-
322
- const result = await executeTool(toolCall);
323
-
324
- // Emit tool_result event
325
- onEvent?.({
326
- type: "tool_result",
327
- tool: toolName,
328
- status: result.success ? "complete" : "error",
329
- result,
330
- });
331
-
332
- if (!result.success) {
333
- return {
334
- handled: true,
335
- toolName,
336
- toolArgs,
337
- toolResult: result,
338
- summaryPrompt: `도구 "${toolName}" 실행 중 오류가 발생했습니다: ${result.error}\n사용자에게 오류 내용을 설명해주세요.`,
339
- };
340
- }
341
-
342
- // Compress result to fit in context window
343
- const compressed = compressAnalysisResult(result.content);
344
-
345
- return {
346
- handled: true,
347
- toolName,
348
- toolArgs,
349
- toolResult: result,
350
- summaryPrompt: `아래는 "${toolName}" 도구의 실행 결과입니다. 사용자에게 한국어로 핵심 내용을 요약해주세요.\n\n${compressed}`,
351
- };
352
- }
353
-
354
- /**
355
- * Compress analysis result JSON to fit within LLM context window.
356
- * Extracts only key metrics, removing verbose raw data.
357
- */
358
- function compressAnalysisResult(resultContent: string, maxChars: number = 2000): string {
359
- try {
360
- const parsed = JSON.parse(resultContent);
361
-
362
- // analyze_all result
363
- if (parsed.path && parsed.fileStats) {
364
- const compact: Record<string, unknown> = {
365
- path: parsed.path,
366
- totalFiles: parsed.totalFiles,
367
- fileStats: parsed.fileStats,
368
- analysesRun: parsed.analysesRun,
369
- successful: parsed.successful,
370
- failed: parsed.failed,
371
- };
372
-
373
- // Extract issue summaries (compact)
374
- if (parsed.issuesSummary?.length > 0) {
375
- compact.issues = parsed.issuesSummary.map((is: { tool: string; issues: string[] }) => ({
376
- tool: is.tool,
377
- issues: is.issues.slice(0, 5),
378
- }));
379
- }
380
-
381
- // Extract per-tool summaries (key metrics only)
382
- if (parsed.details?.length > 0) {
383
- compact.analyses = parsed.details.map((d: { tool: string; summary: Record<string, unknown> }) => {
384
- const s = d.summary;
385
- const brief: Record<string, unknown> = { tool: d.tool };
386
-
387
- // Extract numeric/small fields only
388
- for (const [k, v] of Object.entries(s)) {
389
- if (typeof v === "number" || typeof v === "boolean") {
390
- brief[k] = v;
391
- } else if (typeof v === "string" && v.length < 100) {
392
- brief[k] = v;
393
- }
394
- // Skip arrays/objects (raw data) to save space
395
- }
396
-
397
- // Include issues from samples (java_analyze etc.)
398
- if (Array.isArray((s as any).samples)) {
399
- const allIssues: unknown[] = [];
400
- for (const sample of (s as any).samples) {
401
- if (Array.isArray(sample.result?.issues)) {
402
- allIssues.push(...sample.result.issues.slice(0, 3));
403
- }
404
- }
405
- if (allIssues.length > 0) {
406
- brief.issues = allIssues.slice(0, 10);
407
- }
408
- }
409
-
410
- return brief;
411
- });
412
- }
413
-
414
- if (parsed.errors?.length > 0) {
415
- compact.errors = parsed.errors;
416
- }
417
-
418
- const result = JSON.stringify(compact, null, 1);
419
- return result.length > maxChars ? result.slice(0, maxChars) + "..." : result;
420
- }
421
-
422
- // java_analyze or other single-file results
423
- if (parsed.file || parsed.filepath || parsed.classes || parsed.functions) {
424
- const result = JSON.stringify(parsed, null, 1);
425
- return result.length > maxChars ? result.slice(0, maxChars) + "..." : result;
426
- }
427
-
428
- // Generic: just truncate
429
- const result = JSON.stringify(parsed, null, 1);
430
- return result.length > maxChars ? result.slice(0, maxChars) + "..." : result;
431
- } catch {
432
- // Not valid JSON, return truncated raw text
433
- return resultContent.length > maxChars ? resultContent.slice(0, maxChars) + "..." : resultContent;
434
- }
435
- }
436
-
437
- // ─── Main processing functions ───
438
-
439
- export async function processMessage(
440
- userMessage: string,
441
- history: ChatMessage[],
442
- client: OllamaClient,
443
- config: Config,
444
- onEvent?: (event: AgentEvent) => void,
445
- contextSummary?: string
446
- ): Promise<AgentResult> {
447
- // Try intent router first
448
- const intent = await detectAndExecuteIntent(userMessage, onEvent);
449
-
450
- if (intent.handled && intent.summaryPrompt) {
451
- // Tool already executed → ask LLM to summarize only (no tools = VRAM savings)
452
- onEvent?.({ type: "thinking" });
453
-
454
- const summaryMessages: ChatMessage[] = [
455
- { role: "system", content: BASE_SYSTEM_PROMPT },
456
- { role: "user", content: intent.summaryPrompt },
457
- ];
458
-
459
- const response = await client.chat(summaryMessages); // No tools!
460
-
461
- onEvent?.({ type: "content", content: response.content });
462
- onEvent?.({ type: "done" });
463
-
464
- return {
465
- content: response.content,
466
- toolCalls: intent.toolResult
467
- ? [{ tool: intent.toolName!, args: intent.toolArgs!, result: intent.toolResult }]
468
- : [],
469
- };
470
- }
471
-
472
- // Fallback: existing LLM-driven tool selection
473
- const tools = selectTools(userMessage);
474
- const systemPrompt = buildSystemPrompt(contextSummary);
475
-
476
- const messages: ChatMessage[] = [
477
- { role: "system", content: systemPrompt },
478
- ...history,
479
- { role: "user", content: userMessage },
480
- ];
481
-
482
- const toolCallResults: AgentResult["toolCalls"] = [];
483
- let finalContent = "";
484
- let iterations = 0;
485
- const maxIterations = 10;
486
-
487
- while (iterations < maxIterations) {
488
- iterations++;
489
-
490
- onEvent?.({ type: "thinking" });
491
-
492
- const response = await client.chat(messages, tools as Tool[]);
493
- messages.push(response);
494
-
495
- // If no tool calls, we're done
496
- if (!response.toolCalls?.length) {
497
- finalContent = response.content;
498
- break;
499
- }
500
-
501
- // Process tool calls
502
- for (const toolCall of response.toolCalls) {
503
- onEvent?.({
504
- type: "tool_use",
505
- tool: toolCall.name,
506
- status: "start",
507
- args: toolCall.arguments,
508
- });
509
-
510
- const result = await executeTool(toolCall);
511
-
512
- onEvent?.({
513
- type: "tool_result",
514
- tool: toolCall.name,
515
- status: result.success ? "complete" : "error",
516
- result,
517
- });
518
-
519
- toolCallResults.push({
520
- tool: toolCall.name,
521
- args: toolCall.arguments,
522
- result,
523
- });
524
-
525
- // Add tool result to messages
526
- messages.push({
527
- role: "tool",
528
- content: result.success ? result.content : `Error: ${result.error}`,
529
- toolCallId: toolCall.id,
530
- });
531
- }
532
-
533
- // Continue the conversation with tool results
534
- onEvent?.({ type: "content", content: response.content });
535
- }
536
-
537
- if (iterations >= maxIterations) {
538
- onEvent?.({ type: "error", error: "Maximum iterations reached" });
539
- }
540
-
541
- onEvent?.({ type: "done" });
542
-
543
- return {
544
- content: finalContent,
545
- toolCalls: toolCallResults,
546
- };
547
- }
548
-
549
- export async function* streamProcessMessage(
550
- userMessage: string,
551
- history: ChatMessage[],
552
- client: OllamaClient,
553
- config: Config,
554
- abortSignal?: AbortSignal,
555
- contextSummary?: string
556
- ): AsyncGenerator<AgentEvent> {
557
- // Try intent router first
558
- const intent = await detectAndExecuteIntent(userMessage, (event) => {
559
- // Events are yielded by the caller, we collect them via callback
560
- // But generators can't yield from callbacks, so we handle this differently
561
- });
562
-
563
- if (intent.handled) {
564
- // Emit the tool events that happened during intent detection
565
- if (intent.toolName) {
566
- yield {
567
- type: "tool_use",
568
- tool: intent.toolName,
569
- status: "start",
570
- args: intent.toolArgs,
571
- };
572
-
573
- yield {
574
- type: "tool_result",
575
- tool: intent.toolName,
576
- status: intent.toolResult?.success ? "complete" : "error",
577
- result: intent.toolResult,
578
- };
579
- }
580
-
581
- if (intent.summaryPrompt) {
582
- if (abortSignal?.aborted) {
583
- yield { type: "error", error: "Operation cancelled" };
584
- return;
585
- }
586
-
587
- yield { type: "thinking" };
588
-
589
- // Stream the LLM summary (no tools = streaming mode in ollama client)
590
- const summaryMessages: ChatMessage[] = [
591
- { role: "system", content: BASE_SYSTEM_PROMPT },
592
- { role: "user", content: intent.summaryPrompt },
593
- ];
594
-
595
- for await (const event of client.streamChat(summaryMessages, undefined, abortSignal)) {
596
- if (abortSignal?.aborted) {
597
- yield { type: "error", error: "Operation cancelled" };
598
- return;
599
- }
600
- if (event.type === "content" && event.content) {
601
- yield { type: "content", content: event.content };
602
- } else if (event.type === "error") {
603
- yield { type: "error", error: event.error };
604
- return;
605
- }
606
- }
607
- }
608
-
609
- yield { type: "done" };
610
- return;
611
- }
612
-
613
- // Fallback: existing LLM-driven tool selection
614
- const tools = selectTools(userMessage);
615
- const systemPrompt = buildSystemPrompt(contextSummary);
616
-
617
- const messages: ChatMessage[] = [
618
- { role: "system", content: systemPrompt },
619
- ...history,
620
- { role: "user", content: userMessage },
621
- ];
622
-
623
- let iterations = 0;
624
- const maxIterations = 10;
625
-
626
- while (iterations < maxIterations) {
627
- // Check if aborted
628
- if (abortSignal?.aborted) {
629
- yield { type: "error", error: "Operation cancelled" };
630
- return;
631
- }
632
-
633
- iterations++;
634
-
635
- yield { type: "thinking" };
636
-
637
- let fullContent = "";
638
- const pendingToolCalls: ToolCall[] = [];
639
-
640
- // Collect all events first (non-streaming mode for tools)
641
- for await (const event of client.streamChat(messages, tools as Tool[], abortSignal)) {
642
- if (abortSignal?.aborted) {
643
- yield { type: "error", error: "Operation cancelled" };
644
- return;
645
- }
646
- if (event.type === "content" && event.content) {
647
- fullContent += event.content;
648
- // Don't yield content yet - wait to see if there are tool calls
649
- } else if (event.type === "tool_call" && event.toolCall) {
650
- pendingToolCalls.push(event.toolCall);
651
- } else if (event.type === "error") {
652
- yield { type: "error", error: event.error };
653
- return;
654
- }
655
- }
656
-
657
- // Only yield content if NO tool calls (avoid hallucinated pre-tool text)
658
- if (pendingToolCalls.length === 0 && fullContent) {
659
- yield { type: "content", content: fullContent };
660
- } else if (pendingToolCalls.length > 0) {
661
- // Clear content when tool calls exist
662
- fullContent = "";
663
- }
664
-
665
- messages.push({ role: "assistant", content: fullContent, toolCalls: pendingToolCalls.length > 0 ? pendingToolCalls : undefined });
666
-
667
- // If no tool calls, we're done
668
- if (pendingToolCalls.length === 0) {
669
- break;
670
- }
671
-
672
- // Process tool calls
673
- for (const toolCall of pendingToolCalls) {
674
- // Check if aborted before each tool call
675
- if (abortSignal?.aborted) {
676
- yield { type: "error", error: "Operation cancelled" };
677
- return;
678
- }
679
-
680
- yield {
681
- type: "tool_use",
682
- tool: toolCall.name,
683
- status: "start",
684
- args: toolCall.arguments,
685
- };
686
-
687
- const result = await executeTool(toolCall);
688
-
689
- // Check if aborted after tool execution
690
- if (abortSignal?.aborted) {
691
- yield { type: "error", error: "Operation cancelled" };
692
- return;
693
- }
694
-
695
- yield {
696
- type: "tool_result",
697
- tool: toolCall.name,
698
- status: result.success ? "complete" : "error",
699
- result,
700
- };
701
-
702
- messages.push({
703
- role: "tool",
704
- content: result.success ? result.content : `Error: ${result.error}`,
705
- toolCallId: toolCall.id,
706
- });
707
- }
708
- }
709
-
710
- yield { type: "done" };
711
- }