activo 0.3.5 → 0.3.7

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/src/core/agent.ts CHANGED
@@ -21,65 +21,83 @@ export interface AgentResult {
21
21
  }>;
22
22
  }
23
23
 
24
- const SYSTEM_PROMPT = `You are ACTIVO, a code quality analyzer that MUST use tools.
24
+ const BASE_SYSTEM_PROMPT = `You are ACTIVO, a code quality analyzer.
25
25
 
26
- ## CRITICAL RULES - NEVER VIOLATE
26
+ ## ABSOLUTE RULE: NO TEXT WHEN CALLING TOOLS
27
27
 
28
- 1. **NEVER FABRICATE RESULTS**: You MUST NOT invent file names, method names, class names, or analysis results. ALL information must come from actual tool execution.
28
+ When you call a tool, output NOTHING else. No text before, no text after. ONLY the tool call.
29
29
 
30
- 2. **ALWAYS CALL TOOLS FIRST**: Before providing ANY analysis, you MUST call the appropriate tool. Do NOT write fake results.
30
+ WRONG (NEVER DO THIS):
31
+ \`\`\`
32
+ 실행 중... ← NO!
33
+ [some explanation] ← NO!
34
+ tool_call(...)
35
+ 결과: ... ← NO! (you don't have results yet)
36
+ \`\`\`
31
37
 
32
- 3. **NO PLANNING OR PROMISES**: Do NOT say "I will analyze", "Let me check", "작업 계획", "실행 순서", "진행 중" etc. Just call the tool immediately.
38
+ CORRECT:
39
+ \`\`\`
40
+ tool_call(...)
41
+ \`\`\`
33
42
 
34
- 4. **ONLY REPORT ACTUAL TOOL OUTPUT**: After a tool returns results, summarize ONLY what the tool actually returned. Never add fictional examples.
43
+ ## AFTER TOOL RETURNS
35
44
 
36
- ## Available Tools
45
+ Only AFTER you receive the actual tool result, you may write a response summarizing what the tool returned.
37
46
 
38
- - analyze_all: 디렉토리 전체 분석 (권장)
39
- - java_analyze, java_complexity, spring_check: Java 분석
40
- - sql_check, mybatis_check: SQL/MyBatis 분석
41
- - ast_analyze, react_check, vue_check, jquery_check: JS/TS 분석
42
- - css_check, html_check: CSS/HTML 분석
43
- - dependency_check, openapi_check, python_check: 기타
44
- - read_file, list_directory, grep_search, glob_search: 파일 작업
47
+ ## HALLUCINATION = FAILURE
45
48
 
46
- ## Correct Behavior
49
+ If you write ANY of these WITHOUT a tool result, you have FAILED:
50
+ - File names (e.g., "UserService.java")
51
+ - Numbers (e.g., "복잡도: 15", "3개 파일")
52
+ - Paths (e.g., "/path/to/file.md")
53
+ - Status messages (e.g., "변환 완료!", "성공")
47
54
 
48
- User: "src/**/*.java 분석해줘"
49
- → IMMEDIATELY call: analyze_all(path="src", include=["java"])
50
- → Then summarize the ACTUAL results returned by the tool
55
+ ## Tools
51
56
 
52
- ## WRONG Behavior (NEVER DO THIS)
57
+ - analyze_all: 코드 분석
58
+ - import_pdf_standards: PDF→마크다운 (pdfPath 필수)
59
+ - import_hwp_standards: HWP→마크다운 (hwpPath 필수)
60
+ - read_file, write_file, list_directory, grep_search, glob_search: 파일
53
61
 
54
- Writing fake file names like "OrderService.java", "UserController.java"
55
- ❌ Making up complexity scores like "복잡도: 15"
56
- ❌ Inventing issues that weren't found by tools
57
- ❌ Saying "실행 결과:" without actually executing tools
58
- ❌ Creating tables with fictional data
62
+ ## Example
59
63
 
60
- ## Response Format
64
+ User: "HWP 파일을 마크다운으로 변환해줘"
61
65
 
62
- 1. Call the appropriate tool(s)
63
- 2. Wait for actual results
64
- 3. Summarize ONLY what the tool returned
65
- 4. Use Korean if user speaks Korean`;
66
+ YOUR RESPONSE (no other text):
67
+ Call import_hwp_standards with hwpPath
68
+
69
+ AFTER tool returns result:
70
+ → Now you can summarize the actual result`;
71
+
72
+ // Build system prompt with optional context
73
+ function buildSystemPrompt(contextSummary?: string): string {
74
+ if (!contextSummary) {
75
+ return BASE_SYSTEM_PROMPT;
76
+ }
77
+
78
+ return `${BASE_SYSTEM_PROMPT}
79
+
80
+ ## 이전 대화 컨텍스트
81
+
82
+ ${contextSummary}
83
+
84
+ ---
85
+ 위 내용은 이전 세션에서의 대화 요약입니다. 필요시 참고하세요.`;
86
+ }
66
87
 
67
88
  export async function processMessage(
68
89
  userMessage: string,
69
90
  history: ChatMessage[],
70
91
  client: OllamaClient,
71
92
  config: Config,
72
- onEvent?: (event: AgentEvent) => void
93
+ onEvent?: (event: AgentEvent) => void,
94
+ contextSummary?: string
73
95
  ): Promise<AgentResult> {
74
96
  const tools = getAllTools();
75
- const toolDefinitions = tools.map((t) => ({
76
- name: t.name,
77
- description: t.description,
78
- parameters: t.parameters,
79
- }));
97
+ const systemPrompt = buildSystemPrompt(contextSummary);
80
98
 
81
99
  const messages: ChatMessage[] = [
82
- { role: "system", content: SYSTEM_PROMPT },
100
+ { role: "system", content: systemPrompt },
83
101
  ...history,
84
102
  { role: "user", content: userMessage },
85
103
  ];
@@ -156,12 +174,14 @@ export async function* streamProcessMessage(
156
174
  history: ChatMessage[],
157
175
  client: OllamaClient,
158
176
  config: Config,
159
- abortSignal?: AbortSignal
177
+ abortSignal?: AbortSignal,
178
+ contextSummary?: string
160
179
  ): AsyncGenerator<AgentEvent> {
161
180
  const tools = getAllTools();
181
+ const systemPrompt = buildSystemPrompt(contextSummary);
162
182
 
163
183
  const messages: ChatMessage[] = [
164
- { role: "system", content: SYSTEM_PROMPT },
184
+ { role: "system", content: systemPrompt },
165
185
  ...history,
166
186
  { role: "user", content: userMessage },
167
187
  ];
@@ -0,0 +1,235 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { ChatMessage, OllamaClient } from "./llm/ollama.js";
4
+
5
+ // Conversation storage directory
6
+ const CONVERSATION_DIR = ".activo/conversations";
7
+
8
+ // Session data interface
9
+ interface SessionData {
10
+ id: string;
11
+ startedAt: string;
12
+ updatedAt: string;
13
+ messages: ChatMessage[];
14
+ summary?: string;
15
+ }
16
+
17
+ // Get conversation directory path
18
+ function getConversationDir(): string {
19
+ return path.resolve(process.cwd(), CONVERSATION_DIR);
20
+ }
21
+
22
+ // Ensure conversation directory exists
23
+ function ensureConversationDir(): void {
24
+ const dir = getConversationDir();
25
+ if (!fs.existsSync(dir)) {
26
+ fs.mkdirSync(dir, { recursive: true });
27
+ }
28
+ }
29
+
30
+ // Generate session ID
31
+ export function generateSessionId(): string {
32
+ return `session_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
33
+ }
34
+
35
+ // Get session file path
36
+ function getSessionPath(sessionId: string): string {
37
+ return path.join(getConversationDir(), `${sessionId}.json`);
38
+ }
39
+
40
+ // Get latest session file
41
+ function getLatestSessionPath(): string | null {
42
+ const dir = getConversationDir();
43
+ if (!fs.existsSync(dir)) {
44
+ return null;
45
+ }
46
+
47
+ const files = fs.readdirSync(dir)
48
+ .filter(f => f.startsWith("session_") && f.endsWith(".json"))
49
+ .sort()
50
+ .reverse();
51
+
52
+ if (files.length === 0) {
53
+ return null;
54
+ }
55
+
56
+ return path.join(dir, files[0]);
57
+ }
58
+
59
+ // Load session data
60
+ export function loadSession(sessionId: string): SessionData | null {
61
+ const sessionPath = getSessionPath(sessionId);
62
+ if (!fs.existsSync(sessionPath)) {
63
+ return null;
64
+ }
65
+
66
+ try {
67
+ return JSON.parse(fs.readFileSync(sessionPath, "utf-8"));
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ // Load latest session
74
+ export function loadLatestSession(): SessionData | null {
75
+ const latestPath = getLatestSessionPath();
76
+ if (!latestPath) {
77
+ return null;
78
+ }
79
+
80
+ try {
81
+ return JSON.parse(fs.readFileSync(latestPath, "utf-8"));
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ // Save session data
88
+ export function saveSession(session: SessionData): void {
89
+ ensureConversationDir();
90
+ session.updatedAt = new Date().toISOString();
91
+ fs.writeFileSync(getSessionPath(session.id), JSON.stringify(session, null, 2));
92
+ }
93
+
94
+ // Create new session
95
+ export function createSession(): SessionData {
96
+ return {
97
+ id: generateSessionId(),
98
+ startedAt: new Date().toISOString(),
99
+ updatedAt: new Date().toISOString(),
100
+ messages: [],
101
+ };
102
+ }
103
+
104
+ // Add message to session
105
+ export function addMessageToSession(session: SessionData, message: ChatMessage): void {
106
+ session.messages.push(message);
107
+ saveSession(session);
108
+ }
109
+
110
+ // Summarize old messages using LLM
111
+ async function summarizeMessages(
112
+ messages: ChatMessage[],
113
+ client: OllamaClient
114
+ ): Promise<string> {
115
+ if (messages.length === 0) {
116
+ return "";
117
+ }
118
+
119
+ // Format messages for summarization
120
+ const conversationText = messages
121
+ .filter(m => m.role !== "system" && m.role !== "tool")
122
+ .map(m => {
123
+ if (m.role === "user") {
124
+ return `사용자: ${m.content}`;
125
+ } else if (m.role === "assistant") {
126
+ const toolInfo = m.toolCalls?.length
127
+ ? ` [도구 호출: ${m.toolCalls.map(t => t.name).join(", ")}]`
128
+ : "";
129
+ return `어시스턴트: ${m.content.slice(0, 200)}${m.content.length > 200 ? "..." : ""}${toolInfo}`;
130
+ }
131
+ return "";
132
+ })
133
+ .filter(s => s)
134
+ .join("\n");
135
+
136
+ const summaryPrompt = `다음 대화를 3-5개의 핵심 포인트로 요약해주세요. 한국어로 작성하고, 각 포인트는 한 줄로 작성하세요.
137
+
138
+ 대화:
139
+ ${conversationText}
140
+
141
+ 요약 (핵심 포인트만):`;
142
+
143
+ try {
144
+ const response = await client.chat([
145
+ { role: "user", content: summaryPrompt }
146
+ ]);
147
+ return response.content.trim();
148
+ } catch (error) {
149
+ // Fallback: simple extraction
150
+ const userMessages = messages
151
+ .filter(m => m.role === "user")
152
+ .map(m => m.content.slice(0, 50))
153
+ .slice(-3);
154
+ return `이전 요청: ${userMessages.join(", ")}`;
155
+ }
156
+ }
157
+
158
+ // Get context for new session (hybrid approach)
159
+ export async function getSessionContext(
160
+ client: OllamaClient,
161
+ recentCount: number = 5
162
+ ): Promise<{ summary: string; recentMessages: ChatMessage[] }> {
163
+ const latestSession = loadLatestSession();
164
+
165
+ if (!latestSession || latestSession.messages.length === 0) {
166
+ return { summary: "", recentMessages: [] };
167
+ }
168
+
169
+ const allMessages = latestSession.messages;
170
+
171
+ // Filter out system messages for context
172
+ const contextMessages = allMessages.filter(m => m.role !== "system");
173
+
174
+ if (contextMessages.length <= recentCount) {
175
+ // Not enough messages to summarize, return all as recent
176
+ return {
177
+ summary: latestSession.summary || "",
178
+ recentMessages: contextMessages
179
+ };
180
+ }
181
+
182
+ // Split: old messages for summary, recent messages to keep
183
+ const oldMessages = contextMessages.slice(0, -recentCount);
184
+ const recentMessages = contextMessages.slice(-recentCount);
185
+
186
+ // Generate or use existing summary
187
+ let summary = latestSession.summary || "";
188
+
189
+ if (oldMessages.length > 0 && !summary) {
190
+ summary = await summarizeMessages(oldMessages, client);
191
+ // Save summary back to session
192
+ latestSession.summary = summary;
193
+ saveSession(latestSession);
194
+ }
195
+
196
+ return { summary, recentMessages };
197
+ }
198
+
199
+ // Build context string for system prompt
200
+ export function buildContextPrompt(summary: string): string {
201
+ if (!summary) {
202
+ return "";
203
+ }
204
+
205
+ return `
206
+ ## 이전 대화 컨텍스트
207
+
208
+ ${summary}
209
+
210
+ ---
211
+ 위 내용은 이전 세션에서의 대화 요약입니다. 필요시 참고하세요.
212
+ `;
213
+ }
214
+
215
+ // Clean old sessions (keep only last N)
216
+ export function cleanOldSessions(keepCount: number = 10): void {
217
+ const dir = getConversationDir();
218
+ if (!fs.existsSync(dir)) {
219
+ return;
220
+ }
221
+
222
+ const files = fs.readdirSync(dir)
223
+ .filter(f => f.startsWith("session_") && f.endsWith(".json"))
224
+ .sort()
225
+ .reverse();
226
+
227
+ // Delete old sessions beyond keepCount
228
+ for (let i = keepCount; i < files.length; i++) {
229
+ try {
230
+ fs.unlinkSync(path.join(dir, files[i]));
231
+ } catch {
232
+ // Ignore deletion errors
233
+ }
234
+ }
235
+ }