@pheem49/mint 1.5.2 → 1.5.3

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.
@@ -1,17 +1,16 @@
1
- const { GoogleGenAI } = require('@google/genai');
2
1
  const { readConfig } = require('../System/config_manager');
3
2
  const { performWebAutomation } = require('../Automation_Layer/browser_automation');
4
3
  const { createFolder, deleteFile } = require('../Automation_Layer/file_operations');
5
4
  const { searchKnowledge } = require('./knowledge_base');
6
5
  const safetyManager = require('../System/safety_manager');
6
+ const providerAdapter = require('./provider_adapter');
7
+ const taskManager = require('../System/task_manager');
7
8
  const fs = require('fs');
8
9
  const path = require('path');
9
10
 
10
11
  const os = require('os');
11
12
  const { sendNotification } = require('../System/notifications');
12
13
 
13
- const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
14
-
15
14
  function expandHome(filePath) {
16
15
  if (filePath.startsWith('~/')) {
17
16
  return path.join(os.homedir(), filePath.slice(2));
@@ -47,24 +46,23 @@ TOOL DETAILS:
47
46
  - "done": Target is the final summary of what was accomplished.
48
47
  `;
49
48
 
50
- async function executeAutonomousTask(taskDescription, notifyCallback) {
49
+ async function executeAutonomousTask(taskDescription, notifyCallback, options = {}) {
51
50
  const config = readConfig();
52
- const modelName = config.geminiModel || DEFAULT_GEMINI_MODEL;
53
- const apiKey = config.apiKey || process.env.GEMINI_API_KEY;
54
-
55
- // Use the custom chat creation pattern from the project
56
- const ai = new GoogleGenAI({ apiKey });
57
- const chat = ai.chats.create({
58
- model: modelName,
59
- config: {
60
- systemInstruction: AUTONOMOUS_SYSTEM_PROMPT,
61
- responseMimeType: "application/json"
62
- },
63
- history: []
51
+ const providerOrder = providerAdapter.getProviderAttemptOrder(config, {
52
+ supported: ['gemini', 'anthropic', 'openai', 'local_openai'],
53
+ priority: ['anthropic', 'openai', 'gemini', 'local_openai']
54
+ });
55
+ const client = new providerAdapter.AgentProviderClient({
56
+ provider: providerOrder[0],
57
+ providerOrder,
58
+ config,
59
+ systemInstruction: AUTONOMOUS_SYSTEM_PROMPT,
60
+ responseMimeType: 'application/json',
61
+ maxTokens: 4096
64
62
  });
65
63
 
66
64
  let currentObservation = `Task: ${taskDescription}\nWhat is your first step?`;
67
- let maxSteps = 10;
65
+ let maxSteps = Number.isFinite(options.maxSteps) ? options.maxSteps : 10;
68
66
  let step = 0;
69
67
  let result = null;
70
68
 
@@ -73,12 +71,20 @@ async function executeAutonomousTask(taskDescription, notifyCallback) {
73
71
  if (notifyCallback) notifyCallback(`Step ${step}: Thinking...`);
74
72
 
75
73
  try {
76
- const response = await chat.sendMessage({ message: [{ text: currentObservation }] });
77
- const text = response.text;
74
+ const text = await client.sendMessage(currentObservation);
78
75
  const actionObj = JSON.parse(text);
79
76
 
80
77
  console.log(`[Brain] Thought: ${actionObj.thought}`);
81
78
  console.log(`[Brain] Action: ${actionObj.action} -> ${actionObj.target}`);
79
+ if (options.taskId) {
80
+ taskManager.addCheckpoint(options.taskId, {
81
+ phase: 'autonomous_step',
82
+ step,
83
+ thought: actionObj.thought,
84
+ action: actionObj.action,
85
+ target: actionObj.target
86
+ });
87
+ }
82
88
 
83
89
  if (actionObj.action === 'done') {
84
90
  result = actionObj.target;
@@ -111,6 +117,13 @@ async function executeAutonomousTask(taskDescription, notifyCallback) {
111
117
  approved: true
112
118
  });
113
119
  fs.writeFileSync(filePath, actionObj.data || '');
120
+ if (options.taskId) {
121
+ taskManager.addArtifact(options.taskId, {
122
+ type: 'file',
123
+ path: filePath,
124
+ description: `Written by autonomous task step ${step}`
125
+ });
126
+ }
114
127
  observation = `File written successfully to ${actionObj.target}`;
115
128
  } catch (e) {
116
129
  observation = `Failed to write file: ${e.message}`;
@@ -139,10 +152,24 @@ async function executeAutonomousTask(taskDescription, notifyCallback) {
139
152
  }
140
153
 
141
154
  currentObservation = `Observation: ${observation}`;
155
+ if (options.taskId) {
156
+ taskManager.addCheckpoint(options.taskId, {
157
+ phase: 'observation',
158
+ step,
159
+ message: observation
160
+ });
161
+ }
142
162
 
143
163
  } catch (err) {
144
164
  console.error('[AutonomousBrain] Error during loop:', err);
145
165
  currentObservation = `Error occurred: ${err.message}. Please try a different approach or conclude if task is impossible.`;
166
+ if (options.taskId) {
167
+ taskManager.addCheckpoint(options.taskId, {
168
+ phase: 'error',
169
+ step,
170
+ message: err.message
171
+ });
172
+ }
146
173
  }
147
174
  }
148
175
 
@@ -23,6 +23,10 @@ let isProcessingTask = false;
23
23
  async function startAgent() {
24
24
  console.log(`\n${colors.mint}${colors.bright}[Mint-Agent] Background agent started.${colors.reset}`);
25
25
  console.log(`${colors.gray}[Mint-Agent] Monitoring system events and task queue...${colors.reset}\n`);
26
+ const resumed = taskManager.resumeRunningTasks();
27
+ if (resumed.length > 0) {
28
+ console.log(`${colors.gray}[Mint-Agent] Re-queued ${resumed.length} interrupted task(s).${colors.reset}`);
29
+ }
26
30
 
27
31
  // Initialize System Monitoring
28
32
  systemEvents.startMonitoring();
@@ -70,25 +74,40 @@ async function checkTaskQueue() {
70
74
  console.log(`\n${colors.mint}[Agent] Picking up task: ${task.description}${colors.reset}`);
71
75
 
72
76
  taskManager.updateTask(task.id, { status: 'running' });
77
+ taskManager.addCheckpoint(task.id, {
78
+ phase: 'started',
79
+ message: task.description
80
+ });
73
81
  sendNotification("🚀 เริ่มทำงานให้แล้วนะคะ", `กำลังดำเนินการ: ${task.description}`);
74
82
 
75
83
  try {
76
84
  const result = await executeAutonomousTask(task.description, (progress) => {
77
85
  console.log(`${colors.gray}[Progress] ${progress}${colors.reset}`);
86
+ taskManager.addCheckpoint(task.id, {
87
+ phase: 'progress',
88
+ message: progress
89
+ });
78
90
  // Send periodic progress notifications if important
79
91
  if (progress.includes('เสนอให้รันคำสั่ง')) {
80
92
  sendNotification("💡 มิ้นท์มีข้อแนะนำค่ะ", progress);
81
93
  }
82
- });
94
+ }, { taskId: task.id });
83
95
 
96
+ taskManager.addArtifact(task.id, {
97
+ type: 'final_result',
98
+ content: result
99
+ });
84
100
  taskManager.updateTask(task.id, { status: 'completed', result });
85
101
  sendNotification("✅ งานเสร็จเรียบร้อยแล้วค่ะ!", result);
86
102
  console.log(`\n${colors.mint}[Agent] Task completed successfully.${colors.reset}`);
87
103
 
88
104
  } catch (err) {
89
105
  console.error('[Agent] Task execution failed:', err);
90
- taskManager.updateTask(task.id, { status: 'failed', result: err.message });
106
+ const next = taskManager.failTaskWithRetry(task.id, err.message);
91
107
  sendNotification("❌ เกิดข้อผิดพลาดในการทำงาน", err.message);
108
+ if (next && next.status === 'pending') {
109
+ console.log(`${colors.gray}[Agent] Task scheduled for retry (${next.retryCount}/${next.maxRetries}).${colors.reset}`);
110
+ }
92
111
  } finally {
93
112
  isProcessingTask = false;
94
113
  }
@@ -0,0 +1,358 @@
1
+ const axios = require('axios');
2
+ const { GoogleGenAI } = require('@google/genai');
3
+ const { getAvailableProviders } = require('../System/config_manager');
4
+
5
+ const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
6
+ const ALL_PROVIDERS = ['anthropic', 'openai', 'gemini', 'local_openai', 'ollama', 'huggingface'];
7
+
8
+ function splitDataUri(dataUri = '') {
9
+ const match = String(dataUri).match(/^data:([^;]+);base64,([\s\S]+)$/);
10
+ if (!match) return null;
11
+ return {
12
+ mimeType: match[1],
13
+ data: match[2]
14
+ };
15
+ }
16
+
17
+ function contentToText(content) {
18
+ if (content && typeof content === 'object' && !Array.isArray(content)) {
19
+ return String(content.text || '');
20
+ }
21
+ return String(content || '');
22
+ }
23
+
24
+ function contentToGeminiParts(content) {
25
+ const text = contentToText(content);
26
+ const parts = text ? [{ text }] : [];
27
+ if (content && typeof content === 'object') {
28
+ const images = Array.isArray(content.imageDataUris)
29
+ ? content.imageDataUris
30
+ : (content.imageDataUri ? [content.imageDataUri] : []);
31
+ for (const item of images) {
32
+ const image = splitDataUri(item);
33
+ if (image) parts.push({ inlineData: { mimeType: image.mimeType, data: image.data } });
34
+ }
35
+ if (content.audioDataUri) {
36
+ const audio = splitDataUri(content.audioDataUri);
37
+ if (audio) parts.push({ inlineData: { mimeType: audio.mimeType, data: audio.data } });
38
+ }
39
+ }
40
+ return parts.length > 0 ? parts : [{ text: '' }];
41
+ }
42
+
43
+ function contentToOpenAIContent(content) {
44
+ const text = contentToText(content) || 'Analyze this input.';
45
+ if (content && typeof content === 'object') {
46
+ const images = Array.isArray(content.imageDataUris)
47
+ ? content.imageDataUris
48
+ : (content.imageDataUri ? [content.imageDataUri] : []);
49
+ if (images.length > 0) {
50
+ return [
51
+ { type: 'text', text },
52
+ ...images.map(item => ({ type: 'image_url', image_url: { url: item } }))
53
+ ];
54
+ }
55
+ }
56
+ return text;
57
+ }
58
+
59
+ function contentToAnthropicContent(content) {
60
+ const text = contentToText(content) || 'Analyze this input.';
61
+ if (content && typeof content === 'object') {
62
+ const images = Array.isArray(content.imageDataUris)
63
+ ? content.imageDataUris
64
+ : (content.imageDataUri ? [content.imageDataUri] : []);
65
+ if (images.length > 0) {
66
+ const blocks = [];
67
+ for (const item of images) {
68
+ const image = splitDataUri(item);
69
+ if (image) {
70
+ blocks.push({ type: 'image', source: { type: 'base64', media_type: image.mimeType, data: image.data } });
71
+ }
72
+ }
73
+ blocks.push({ type: 'text', text });
74
+ return blocks;
75
+ }
76
+ }
77
+ return text;
78
+ }
79
+
80
+ function contentToOllamaMessage(content) {
81
+ const text = contentToText(content) || 'Analyze this input.';
82
+ const message = { role: 'user', content: text };
83
+ if (content && typeof content === 'object') {
84
+ const images = Array.isArray(content.imageDataUris)
85
+ ? content.imageDataUris
86
+ : (content.imageDataUri ? [content.imageDataUri] : []);
87
+ const imagePayloads = images
88
+ .map(item => splitDataUri(item))
89
+ .filter(Boolean)
90
+ .map(image => image.data);
91
+ if (imagePayloads.length > 0) message.images = imagePayloads;
92
+ }
93
+ return message;
94
+ }
95
+
96
+ function getProviderAttemptOrder(config = {}, options = {}) {
97
+ const supported = options.supported || ALL_PROVIDERS;
98
+ const available = (options.availableProviders || getAvailableProviders(config))
99
+ .filter(provider => supported.includes(provider));
100
+ const requested = options.requested || config.aiProvider || 'gemini';
101
+ const priority = (options.priority || ALL_PROVIDERS).filter(provider => supported.includes(provider));
102
+ const ordered = [];
103
+
104
+ if (supported.includes(requested) && available.includes(requested)) {
105
+ ordered.push(requested);
106
+ }
107
+
108
+ for (const provider of priority) {
109
+ if (available.includes(provider) && !ordered.includes(provider)) {
110
+ ordered.push(provider);
111
+ }
112
+ }
113
+
114
+ return ordered.length > 0 ? ordered : ['gemini'];
115
+ }
116
+
117
+ function getProviderModel(provider, config = {}) {
118
+ switch (provider) {
119
+ case 'anthropic':
120
+ return config.anthropicModel || 'claude-3-5-sonnet-latest';
121
+ case 'openai':
122
+ return config.openaiModel || 'gpt-4o';
123
+ case 'local_openai':
124
+ return config.localModelName || 'local-model';
125
+ case 'ollama':
126
+ return config.ollamaModel || 'llama3:latest';
127
+ case 'huggingface':
128
+ return config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct';
129
+ case 'gemini':
130
+ default:
131
+ return config.geminiModel || DEFAULT_GEMINI_MODEL;
132
+ }
133
+ }
134
+
135
+ class AgentProviderClient {
136
+ constructor(options = {}) {
137
+ this.provider = options.provider || 'gemini';
138
+ this.providerOrder = options.providerOrder && options.providerOrder.length
139
+ ? options.providerOrder
140
+ : [this.provider];
141
+ this.config = options.config || {};
142
+ this.history = options.history || [];
143
+ this.systemInstruction = options.systemInstruction || '';
144
+ this.responseMimeType = options.responseMimeType || 'application/json';
145
+ this.maxTokens = options.maxTokens || 8192;
146
+ this.lastSuccessfulProvider = null;
147
+ this.usageTotals = {};
148
+ }
149
+
150
+ recordUsage(provider, model, usage = {}) {
151
+ const key = `${provider}:${model || ''}`;
152
+ if (!this.usageTotals[key]) {
153
+ this.usageTotals[key] = {
154
+ provider,
155
+ model,
156
+ requests: 0,
157
+ inputTokens: 0,
158
+ cacheReads: 0,
159
+ outputTokens: 0,
160
+ reasoningTokens: 0,
161
+ totalTokens: 0
162
+ };
163
+ }
164
+
165
+ const row = this.usageTotals[key];
166
+ row.requests += 1;
167
+ row.inputTokens += Number(usage.inputTokens) || 0;
168
+ row.cacheReads += Number(usage.cacheReads) || 0;
169
+ row.outputTokens += Number(usage.outputTokens) || 0;
170
+ row.reasoningTokens += Number(usage.reasoningTokens) || 0;
171
+ row.totalTokens += Number(usage.totalTokens) || 0;
172
+ }
173
+
174
+ getUsageSummary() {
175
+ return Object.values(this.usageTotals);
176
+ }
177
+
178
+ async sendMessage(observation) {
179
+ this.history.push({ role: 'user', content: observation });
180
+
181
+ const failures = [];
182
+ for (const provider of this.providerOrder) {
183
+ this.provider = provider;
184
+ try {
185
+ let responseText = '';
186
+ if (provider === 'anthropic') responseText = await this.callAnthropic();
187
+ else if (provider === 'openai' || provider === 'local_openai') responseText = await this.callOpenAI();
188
+ else if (provider === 'ollama') responseText = await this.callOllama();
189
+ else if (provider === 'huggingface') responseText = await this.callHuggingFace();
190
+ else responseText = await this.callGemini();
191
+
192
+ this.history.push({ role: 'assistant', content: responseText });
193
+ this.lastSuccessfulProvider = provider;
194
+ return responseText;
195
+ } catch (error) {
196
+ const message = error.message || error.code || 'unknown error';
197
+ failures.push(`${provider}: ${message}`);
198
+ if (process.env.MINT_DEBUG === '1') {
199
+ console.error(`[ProviderAdapter] Provider '${provider}' failed: ${message}`);
200
+ }
201
+ }
202
+ }
203
+
204
+ throw new Error(`All providers failed. ${failures.join(' | ')}`);
205
+ }
206
+
207
+ async callAnthropic() {
208
+ const apiKey = this.config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
209
+ const model = getProviderModel('anthropic', this.config);
210
+ const messages = this.history.map(m => ({
211
+ role: m.role,
212
+ content: contentToAnthropicContent(m.content)
213
+ }));
214
+
215
+ const response = await axios.post('https://api.anthropic.com/v1/messages', {
216
+ model,
217
+ max_tokens: this.maxTokens,
218
+ system: this.systemInstruction,
219
+ messages
220
+ }, {
221
+ headers: {
222
+ 'x-api-key': apiKey,
223
+ 'anthropic-version': '2023-06-01',
224
+ 'content-type': 'application/json'
225
+ }
226
+ });
227
+ const usage = response.data.usage || {};
228
+ this.recordUsage('anthropic', model, {
229
+ inputTokens: usage.input_tokens,
230
+ cacheReads: usage.cache_read_input_tokens,
231
+ outputTokens: usage.output_tokens,
232
+ totalTokens: (Number(usage.input_tokens) || 0) + (Number(usage.output_tokens) || 0)
233
+ });
234
+ return response.data.content[0].text;
235
+ }
236
+
237
+ async callOpenAI() {
238
+ const isLocal = this.provider === 'local_openai';
239
+ const apiKey = isLocal ? 'not-needed' : (this.config.openaiApiKey || process.env.OPENAI_API_KEY);
240
+ const baseUrl = isLocal ? (this.config.localApiBaseUrl || 'http://localhost:1234/v1') : 'https://api.openai.com/v1';
241
+ const model = getProviderModel(this.provider, this.config);
242
+ const messages = [
243
+ { role: 'system', content: this.systemInstruction },
244
+ ...this.history.map(m => ({
245
+ role: m.role,
246
+ content: contentToOpenAIContent(m.content)
247
+ }))
248
+ ];
249
+
250
+ const response = await axios.post(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
251
+ model,
252
+ messages,
253
+ response_format: isLocal ? undefined : { type: 'json_object' }
254
+ }, {
255
+ headers: {
256
+ 'Authorization': `Bearer ${apiKey}`,
257
+ 'Content-Type': 'application/json'
258
+ }
259
+ });
260
+ const usage = response.data.usage || {};
261
+ this.recordUsage(this.provider, model, {
262
+ inputTokens: usage.prompt_tokens,
263
+ cacheReads: usage.prompt_tokens_details && usage.prompt_tokens_details.cached_tokens,
264
+ outputTokens: usage.completion_tokens,
265
+ reasoningTokens: usage.completion_tokens_details && usage.completion_tokens_details.reasoning_tokens,
266
+ totalTokens: usage.total_tokens
267
+ });
268
+ return response.data.choices[0].message.content;
269
+ }
270
+
271
+ async callGemini() {
272
+ const apiKey = this.config.apiKey || process.env.GEMINI_API_KEY;
273
+ const model = getProviderModel('gemini', this.config);
274
+ const ai = new GoogleGenAI({ apiKey });
275
+ const recentHistory = this.history.slice(-16);
276
+ const priorHistory = recentHistory.slice(0, -1);
277
+ const lastEntry = recentHistory[recentHistory.length - 1] || { content: '' };
278
+ const history = priorHistory.map(m => ({
279
+ role: m.role === 'assistant' ? 'model' : 'user',
280
+ parts: contentToGeminiParts(m.content)
281
+ }));
282
+ const chat = ai.chats.create({
283
+ model,
284
+ config: {
285
+ systemInstruction: this.systemInstruction,
286
+ responseMimeType: this.responseMimeType
287
+ },
288
+ history
289
+ });
290
+
291
+ const response = await chat.sendMessage({ message: contentToGeminiParts(lastEntry.content) });
292
+ const usage = response.usageMetadata || {};
293
+ this.recordUsage('gemini', model, {
294
+ inputTokens: usage.promptTokenCount,
295
+ cacheReads: usage.cachedContentTokenCount,
296
+ outputTokens: usage.candidatesTokenCount,
297
+ reasoningTokens: usage.thoughtsTokenCount,
298
+ totalTokens: usage.totalTokenCount
299
+ });
300
+ return typeof response.text === 'function' ? response.text() : response.text;
301
+ }
302
+
303
+ async callOllama() {
304
+ const model = getProviderModel('ollama', this.config);
305
+ const baseUrl = (this.config.ollamaHost || 'http://localhost:11434').replace(/\/$/, '');
306
+ const messages = [
307
+ { role: 'system', content: this.systemInstruction },
308
+ ...this.history.map(m => m.role === 'assistant'
309
+ ? { role: 'assistant', content: contentToText(m.content) }
310
+ : contentToOllamaMessage(m.content))
311
+ ];
312
+ const response = await axios.post(`${baseUrl}/api/chat`, {
313
+ model,
314
+ messages,
315
+ format: this.responseMimeType === 'application/json' ? 'json' : undefined,
316
+ stream: false
317
+ });
318
+ return response.data.message.content;
319
+ }
320
+
321
+ async callHuggingFace() {
322
+ const apiKey = this.config.hfApiKey || process.env.HF_API_KEY;
323
+ const model = getProviderModel('huggingface', this.config);
324
+ const messages = [
325
+ { role: 'system', content: this.systemInstruction },
326
+ ...this.history.map(m => ({
327
+ role: m.role,
328
+ content: contentToOpenAIContent(m.content)
329
+ }))
330
+ ];
331
+ const response = await axios.post(`https://api-inference.huggingface.co/models/${model}/v1/chat/completions`, {
332
+ model,
333
+ messages,
334
+ max_tokens: this.maxTokens
335
+ }, {
336
+ headers: {
337
+ 'Authorization': `Bearer ${apiKey}`,
338
+ 'Content-Type': 'application/json'
339
+ }
340
+ });
341
+ return response.data.choices[0].message.content;
342
+ }
343
+ }
344
+
345
+ module.exports = {
346
+ DEFAULT_GEMINI_MODEL,
347
+ ALL_PROVIDERS,
348
+ AgentProviderClient,
349
+ getProviderAttemptOrder,
350
+ getProviderModel,
351
+ _helpers: {
352
+ splitDataUri,
353
+ contentToGeminiParts,
354
+ contentToOpenAIContent,
355
+ contentToAnthropicContent,
356
+ contentToOllamaMessage
357
+ }
358
+ };
@@ -18,6 +18,11 @@ async function requestCodeApproval(request) {
18
18
 
19
19
  console.log(`\n${colors.yellow}${colors.bright}[Approval Required]${colors.reset} ${typeLabel}`);
20
20
  if (request.label) console.log(`${colors.gray}${request.label}${colors.reset}`);
21
+ if (Array.isArray(request.warnings) && request.warnings.length > 0) {
22
+ request.warnings.forEach((warning) => {
23
+ console.log(`${colors.yellow}Warning:${colors.reset} ${warning}`);
24
+ });
25
+ }
21
26
  if (request.preview) console.log(`${colors.gray}${request.preview}${colors.reset}\n`);
22
27
 
23
28
  const rl = readline.createInterface({
@@ -119,6 +119,13 @@ async function detectCodeIntent(text, workspaceRoot = process.cwd(), history = [
119
119
  }
120
120
 
121
121
  const heuristicRoute = detectCodeIntentHeuristic(input, workspaceRoot);
122
+ if (!heuristicRoute) {
123
+ return {
124
+ route: 'chat',
125
+ reason: 'No substantial code intent detected.'
126
+ };
127
+ }
128
+
122
129
  const routerClient = getRouterClient();
123
130
  if (!routerClient) {
124
131
  return {