chore: sync connectai
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "g1nation",
|
"name": "g1nation",
|
||||||
"displayName": "G1nation",
|
"displayName": "G1nation",
|
||||||
"description": "100% local AI coding agent for VS Code. Create files, edit code, run commands, and work offline with Ollama or LM Studio.",
|
"description": "100% local AI coding agent for VS Code. Create files, edit code, run commands, and work offline with Ollama or LM Studio.",
|
||||||
"version": "2.2.27",
|
"version": "2.2.29",
|
||||||
"publisher": "connectailab",
|
"publisher": "connectailab",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
|
|||||||
+114
-68
@@ -9,13 +9,19 @@ import {
|
|||||||
EXCLUDED_DIRS,
|
EXCLUDED_DIRS,
|
||||||
SYSTEM_PROMPT,
|
SYSTEM_PROMPT,
|
||||||
shouldAutoPushBrain,
|
shouldAutoPushBrain,
|
||||||
getSecondBrainRepo
|
getSecondBrainRepo,
|
||||||
|
buildApiUrl,
|
||||||
|
logError,
|
||||||
|
logInfo,
|
||||||
|
resolveEngine,
|
||||||
|
summarizeText
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { validatePath, sanitizeCommand } from './security';
|
import { validatePath, sanitizeCommand } from './security';
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
role: 'user' | 'assistant' | 'system';
|
role: 'user' | 'assistant' | 'system';
|
||||||
content: string | any[];
|
content: string | any[];
|
||||||
|
internal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AgentExecutor {
|
export class AgentExecutor {
|
||||||
@@ -32,7 +38,7 @@ export class AgentExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getHistory() {
|
public getHistory() {
|
||||||
return this.chatHistory;
|
return this.chatHistory.filter(message => !message.internal);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setHistory(history: ChatMessage[]) {
|
public setHistory(history: ChatMessage[]) {
|
||||||
@@ -74,6 +80,10 @@ export class AgentExecutor {
|
|||||||
if (!this.webview) return;
|
if (!this.webview) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (loopDepth === 0) {
|
||||||
|
await this.context.workspaceState.update('lastActionStr', undefined);
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Prepare Context
|
// 1. Prepare Context
|
||||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
const rootPath = workspaceFolders ? workspaceFolders[0].uri.fsPath : '';
|
const rootPath = workspaceFolders ? workspaceFolders[0].uri.fsPath : '';
|
||||||
@@ -91,8 +101,12 @@ export class AgentExecutor {
|
|||||||
|
|
||||||
// 2. Setup History
|
// 2. Setup History
|
||||||
if (prompt !== null) {
|
if (prompt !== null) {
|
||||||
this.chatHistory.push({ role: 'user', content: prompt });
|
if (loopDepth === 0) {
|
||||||
this.webview.postMessage({ type: 'streamChunk', value: '' }); // Trigger UI update if needed
|
this.chatHistory.push({ role: 'user', content: prompt });
|
||||||
|
this.webview.postMessage({ type: 'streamChunk', value: '' }); // Trigger UI update if needed
|
||||||
|
} else {
|
||||||
|
this.chatHistory.push({ role: 'system', content: prompt, internal: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. API Request Setup
|
// 3. API Request Setup
|
||||||
@@ -108,60 +122,24 @@ export class AgentExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inject System Directives
|
// Inject System Directives
|
||||||
if (reqMessages.length > 0) {
|
const internetCtx = internetEnabled
|
||||||
const internetCtx = internetEnabled
|
? `\n\n[CRITICAL: INTERNET ACCESS ENABLED]\nYou can use <read_url> to search. Current time: ${new Date().toLocaleString()}`
|
||||||
? `\n\n[CRITICAL: INTERNET ACCESS ENABLED]\nYou can use <read_url> to search. Current time: ${new Date().toLocaleString()}`
|
: '';
|
||||||
: '';
|
const fullSystemPrompt = `${systemPrompt}${internetCtx}\n\n[CONTEXT]\n${contextBlock}`;
|
||||||
const fullSystemPrompt = `${systemPrompt}${internetCtx}\n\n[CONTEXT]\n${contextBlock}\n${internetCtx}`;
|
const messagesForRequest: ChatMessage[] = [
|
||||||
|
{ role: 'system', content: fullSystemPrompt, internal: true },
|
||||||
const firstUserIdx = reqMessages.findIndex(m => m.role === 'user');
|
...reqMessages
|
||||||
if (firstUserIdx >= 0) {
|
];
|
||||||
let content = reqMessages[firstUserIdx].content;
|
|
||||||
if (typeof content === 'string') {
|
|
||||||
reqMessages[firstUserIdx].content = `${fullSystemPrompt}\n\n[USER QUERY]\n${content}`;
|
|
||||||
if (loopDepth > 0) {
|
|
||||||
reqMessages[firstUserIdx].content = `[Autonomous Step ${loopDepth}/${config.maxAutoSteps}]\n${reqMessages[firstUserIdx].content}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Call AI Engine
|
// 4. Call AI Engine
|
||||||
const isLMStudio = ollamaUrl.includes('1234') || ollamaUrl.includes('v1') || ollamaUrl.includes('localhost');
|
|
||||||
// Note: Many users use LM Studio on localhost, we'll try to be smart or fallback to Ollama format if it fails.
|
|
||||||
|
|
||||||
const apiUrl = isLMStudio ?
|
|
||||||
(ollamaUrl.endsWith('/v1') ? `${ollamaUrl}/chat/completions` : `${ollamaUrl}/v1/chat/completions`) :
|
|
||||||
`${ollamaUrl}/api/chat`;
|
|
||||||
|
|
||||||
this.abortController = new AbortController();
|
this.abortController = new AbortController();
|
||||||
|
const request = await this.createStreamingRequest({
|
||||||
const streamBody = {
|
baseUrl: ollamaUrl,
|
||||||
model: modelName || defaultModel,
|
modelName: modelName || defaultModel,
|
||||||
messages: reqMessages,
|
reqMessages: messagesForRequest,
|
||||||
stream: true,
|
temperature
|
||||||
...(isLMStudio
|
|
||||||
? { max_tokens: 4096, temperature }
|
|
||||||
: { options: { num_ctx: 32768, num_predict: 4096, temperature } }),
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'text/event-stream',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Connection': 'keep-alive'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(streamBody),
|
|
||||||
signal: this.abortController.signal,
|
|
||||||
keepalive: true
|
|
||||||
});
|
});
|
||||||
|
const { response, engine, apiUrl } = request;
|
||||||
if (!response.ok) {
|
|
||||||
const errText = await response.text();
|
|
||||||
throw new Error(`AI Engine error: ${response.status} - ${errText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let aiResponseText = '';
|
let aiResponseText = '';
|
||||||
const reader = response.body?.getReader();
|
const reader = response.body?.getReader();
|
||||||
@@ -185,19 +163,21 @@ export class AgentExecutor {
|
|||||||
try {
|
try {
|
||||||
const raw = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed;
|
const raw = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed;
|
||||||
const json = JSON.parse(raw);
|
const json = JSON.parse(raw);
|
||||||
const token = isLMStudio ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || '';
|
const token = engine === 'lmstudio' ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || '';
|
||||||
if (token) {
|
if (token) {
|
||||||
aiResponseText += token;
|
aiResponseText += token;
|
||||||
this.webview?.postMessage({ type: 'streamChunk', value: token });
|
this.webview?.postMessage({ type: 'streamChunk', value: token });
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e: any) {
|
||||||
|
logError('Failed to parse streaming chunk.', { engine, apiUrl, chunk: summarizeText(trimmed, 300), error: e?.message || String(e) });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.name === 'AbortError') {
|
if (err.name === 'AbortError') {
|
||||||
console.log('[Agent] Generation aborted by user.');
|
logInfo('Generation aborted by user.');
|
||||||
} else {
|
} else {
|
||||||
console.error('[Agent] Stream reading error:', err);
|
logError('Stream reading error.', { engine, apiUrl, error: err?.message || String(err) });
|
||||||
this.webview?.postMessage({ type: 'error', value: `Connection lost: ${err.message}` });
|
this.webview?.postMessage({ type: 'error', value: `Connection lost: ${err.message}` });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,19 +188,25 @@ export class AgentExecutor {
|
|||||||
const trimmed = buffer.trim();
|
const trimmed = buffer.trim();
|
||||||
const raw = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed;
|
const raw = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed;
|
||||||
const json = JSON.parse(raw);
|
const json = JSON.parse(raw);
|
||||||
const token = isLMStudio ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || '';
|
const token = engine === 'lmstudio' ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || '';
|
||||||
if (token) {
|
if (token) {
|
||||||
aiResponseText += token;
|
aiResponseText += token;
|
||||||
this.webview?.postMessage({ type: 'streamChunk', value: token });
|
this.webview?.postMessage({ type: 'streamChunk', value: token });
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e: any) {
|
||||||
|
logError('Failed to parse final streaming buffer.', { engine, apiUrl, buffer: summarizeText(buffer, 300), error: e?.message || String(e) });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loopDepth === 0) this.webview.postMessage({ type: 'streamEnd' });
|
|
||||||
this.chatHistory.push({ role: 'assistant', content: aiResponseText });
|
this.chatHistory.push({ role: 'assistant', content: aiResponseText });
|
||||||
|
|
||||||
// 5. Execute Actions
|
// 5. Execute Actions
|
||||||
const report = await this.executeActions(aiResponseText, rootPath);
|
const report = await this.executeActions(aiResponseText, rootPath);
|
||||||
|
if (!aiResponseText.trim() && report.length === 0) {
|
||||||
|
logError('Model returned an empty response without actions.', { model: modelName || defaultModel, loopDepth });
|
||||||
|
this.webview.postMessage({ type: 'error', value: 'AI model returned an empty response.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (report.length > 0) {
|
if (report.length > 0) {
|
||||||
const reportMsg = `\n\n> ⚙️ **System Action Report** (${loopDepth + 1}/${config.maxAutoSteps})\n> ${report.join("\n> ")}\n\n`;
|
const reportMsg = `\n\n> ⚙️ **System Action Report** (${loopDepth + 1}/${config.maxAutoSteps})\n> ${report.join("\n> ")}\n\n`;
|
||||||
@@ -233,11 +219,11 @@ export class AgentExecutor {
|
|||||||
|
|
||||||
if (currentActionStr === lastActionStr) {
|
if (currentActionStr === lastActionStr) {
|
||||||
this.webview.postMessage({ type: 'streamChunk', value: "\n⚠️ *Stopping to prevent infinite loop.*" });
|
this.webview.postMessage({ type: 'streamChunk', value: "\n⚠️ *Stopping to prevent infinite loop.*" });
|
||||||
if (loopDepth === 0) this.webview.postMessage({ type: 'streamEnd' });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.context.workspaceState.update('lastActionStr', currentActionStr);
|
await this.context.workspaceState.update('lastActionStr', currentActionStr);
|
||||||
|
logInfo('Autonomous loop continuing after actions.', { loopDepth: loopDepth + 1, actions: report });
|
||||||
|
|
||||||
// Explicitly tell the AI to look at the results and continue
|
// Explicitly tell the AI to look at the results and continue
|
||||||
const continuationPrompt = "I have executed your actions. Above is the result. Please analyze it and provide the next step or the final answer.";
|
const continuationPrompt = "I have executed your actions. Above is the result. Please analyze it and provide the next step or the final answer.";
|
||||||
@@ -249,10 +235,70 @@ export class AgentExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
logError('Agent prompt failed.', { error: error?.message || String(error), promptPreview: summarizeText(prompt || '', 200) });
|
||||||
this.webview.postMessage({ type: "error", value: `[Agent Error]: ${error.message}` });
|
this.webview.postMessage({ type: "error", value: `[Agent Error]: ${error.message}` });
|
||||||
|
} finally {
|
||||||
|
if (loopDepth === 0) {
|
||||||
|
this.webview.postMessage({ type: 'streamEnd' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async createStreamingRequest(params: {
|
||||||
|
baseUrl: string;
|
||||||
|
modelName: string;
|
||||||
|
reqMessages: ChatMessage[];
|
||||||
|
temperature: number;
|
||||||
|
}): Promise<{ response: Response; engine: 'lmstudio' | 'ollama'; apiUrl: string }> {
|
||||||
|
const { baseUrl, modelName, reqMessages, temperature } = params;
|
||||||
|
const primaryEngine = resolveEngine(baseUrl);
|
||||||
|
const engines = primaryEngine === 'lmstudio' ? ['lmstudio', 'ollama'] as const : ['ollama', 'lmstudio'] as const;
|
||||||
|
let lastError: Error | null = null;
|
||||||
|
|
||||||
|
for (const engine of engines) {
|
||||||
|
const apiUrl = buildApiUrl(baseUrl, engine, 'chat');
|
||||||
|
const streamBody = {
|
||||||
|
model: modelName,
|
||||||
|
messages: reqMessages,
|
||||||
|
stream: true,
|
||||||
|
...(engine === 'lmstudio'
|
||||||
|
? { max_tokens: 4096, temperature }
|
||||||
|
: { options: { num_ctx: 32768, num_predict: 4096, temperature } }),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
logInfo('AI streaming request started.', { engine, apiUrl, model: modelName, messageCount: reqMessages.length });
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(streamBody),
|
||||||
|
signal: this.abortController?.signal,
|
||||||
|
keepalive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errText = await response.text();
|
||||||
|
lastError = new Error(`AI Engine error (${engine}): ${response.status} - ${summarizeText(errText, 300)}`);
|
||||||
|
logError('AI streaming request returned non-OK status.', { engine, apiUrl, status: response.status, body: summarizeText(errText, 500) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
logInfo('AI streaming request connected.', { engine, apiUrl });
|
||||||
|
return { response, engine, apiUrl };
|
||||||
|
} catch (error: any) {
|
||||||
|
lastError = error instanceof Error ? error : new Error(String(error));
|
||||||
|
logError('AI streaming request failed.', { engine, apiUrl, error: lastError.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError || new Error('Unable to connect to AI engine.');
|
||||||
|
}
|
||||||
|
|
||||||
private async executeActions(aiMessage: string, rootPath: string): Promise<string[]> {
|
private async executeActions(aiMessage: string, rootPath: string): Promise<string[]> {
|
||||||
const report: string[] = [];
|
const report: string[] = [];
|
||||||
let brainModified = false;
|
let brainModified = false;
|
||||||
@@ -330,7 +376,7 @@ export class AgentExecutor {
|
|||||||
const content = fs.readFileSync(absPath, 'utf-8');
|
const content = fs.readFileSync(absPath, 'utf-8');
|
||||||
const preview = content.length > 8000 ? content.slice(0, 8000) + "\n... (truncated)" : content;
|
const preview = content.length > 8000 ? content.slice(0, 8000) + "\n... (truncated)" : content;
|
||||||
report.push(`📖 Read: ${relPath}`);
|
report.push(`📖 Read: ${relPath}`);
|
||||||
this.chatHistory.push({ role: 'user', content: `[Result of read_file ${relPath}]\n\`\`\`\n${preview}\n\`\`\`` });
|
this.chatHistory.push({ role: 'system', content: `[Result of read_file ${relPath}]\n\`\`\`\n${preview}\n\`\`\``, internal: true });
|
||||||
} else {
|
} else {
|
||||||
report.push(`❌ Read failed: ${relPath} not found`);
|
report.push(`❌ Read failed: ${relPath} not found`);
|
||||||
}
|
}
|
||||||
@@ -368,7 +414,7 @@ export class AgentExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
report.push(`📂 Listed: ${relPath}`);
|
report.push(`📂 Listed: ${relPath}`);
|
||||||
this.chatHistory.push({ role: 'user', content: `[Result of list_files ${relPath}]\n${listing}` });
|
this.chatHistory.push({ role: 'system', content: `[Result of list_files ${relPath}]\n${listing}`, internal: true });
|
||||||
}
|
}
|
||||||
} catch (err: any) { report.push(`❌ Listing failed: ${err.message}`); }
|
} catch (err: any) { report.push(`❌ Listing failed: ${err.message}`); }
|
||||||
}
|
}
|
||||||
@@ -392,7 +438,7 @@ export class AgentExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
report.push(`🧠 Brain Listed: ${relPath}`);
|
report.push(`🧠 Brain Listed: ${relPath}`);
|
||||||
this.chatHistory.push({ role: 'user', content: `[Result of list_brain ${relPath}]\n${listing}` });
|
this.chatHistory.push({ role: 'system', content: `[Result of list_brain ${relPath}]\n${listing}`, internal: true });
|
||||||
} else {
|
} else {
|
||||||
report.push(`❌ Brain List failed: ${relPath} not found`);
|
report.push(`❌ Brain List failed: ${relPath} not found`);
|
||||||
}
|
}
|
||||||
@@ -411,7 +457,7 @@ export class AgentExecutor {
|
|||||||
if (targetFile && fs.existsSync(targetFile)) {
|
if (targetFile && fs.existsSync(targetFile)) {
|
||||||
const content = fs.readFileSync(targetFile, 'utf-8');
|
const content = fs.readFileSync(targetFile, 'utf-8');
|
||||||
report.push(`🧠 Brain Read: ${fileName}`);
|
report.push(`🧠 Brain Read: ${fileName}`);
|
||||||
this.chatHistory.push({ role: 'user', content: `[Result of read_brain ${fileName}]\n\`\`\`\n${content}\n\`\`\`` });
|
this.chatHistory.push({ role: 'system', content: `[Result of read_brain ${fileName}]\n\`\`\`\n${content}\n\`\`\``, internal: true });
|
||||||
} else {
|
} else {
|
||||||
report.push(`❌ Brain Read failed: ${fileName} not found in Second Brain`);
|
report.push(`❌ Brain Read failed: ${fileName} not found in Second Brain`);
|
||||||
}
|
}
|
||||||
@@ -429,7 +475,7 @@ export class AgentExecutor {
|
|||||||
const content = text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
const content = text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||||
const preview = content.length > 5000 ? content.slice(0, 5000) + "\n... (truncated)" : content;
|
const preview = content.length > 5000 ? content.slice(0, 5000) + "\n... (truncated)" : content;
|
||||||
report.push(`🌐 Read URL: ${url}`);
|
report.push(`🌐 Read URL: ${url}`);
|
||||||
this.chatHistory.push({ role: 'user', content: `[Result of read_url ${url}]\n${preview}` });
|
this.chatHistory.push({ role: 'system', content: `[Result of read_url ${url}]\n${preview}`, internal: true });
|
||||||
} catch (err: any) { report.push(`❌ URL Read failed: ${err.message}`); }
|
} catch (err: any) { report.push(`❌ URL Read failed: ${err.message}`); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,7 +499,7 @@ export class AgentExecutor {
|
|||||||
execSync(`git commit -m "[G1nation] Knowledge Update"`, { cwd: brainDir });
|
execSync(`git commit -m "[G1nation] Knowledge Update"`, { cwd: brainDir });
|
||||||
execSync(`git push`, { cwd: brainDir });
|
execSync(`git push`, { cwd: brainDir });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Agent] Sync failed:', err);
|
logError('Second Brain sync failed.', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-19
@@ -2,7 +2,17 @@ import * as http from 'http';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
// axios removed
|
// axios removed
|
||||||
import { getConfig, _getBrainDir, _isBrainDirExplicitlySet, findBrainFiles } from './utils';
|
import {
|
||||||
|
getConfig,
|
||||||
|
_getBrainDir,
|
||||||
|
_isBrainDirExplicitlySet,
|
||||||
|
findBrainFiles,
|
||||||
|
buildApiUrl,
|
||||||
|
logError,
|
||||||
|
logInfo,
|
||||||
|
resolveEngine,
|
||||||
|
summarizeText
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
export interface BridgeInterface {
|
export interface BridgeInterface {
|
||||||
injectSystemMessage(msg: string): void;
|
injectSystemMessage(msg: string): void;
|
||||||
@@ -48,7 +58,7 @@ export class BridgeServer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.server.listen(port, '127.0.0.1', () => {
|
this.server.listen(port, '127.0.0.1', () => {
|
||||||
console.log(`[G1nation] Bridge Server active on port ${port}`);
|
logInfo(`Bridge server active on 127.0.0.1:${port}.`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +82,7 @@ export class BridgeServer {
|
|||||||
const parsed = JSON.parse(body);
|
const parsed = JSON.parse(body);
|
||||||
await processor(parsed, res);
|
await processor(parsed, res);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
logError('Bridge request failed.', { url: req.url, method: req.method, body: summarizeText(body), error: e?.message || String(e) });
|
||||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({ error: e.message }));
|
res.end(JSON.stringify({ error: e.message }));
|
||||||
}
|
}
|
||||||
@@ -145,27 +156,53 @@ export class BridgeServer {
|
|||||||
|
|
||||||
private async callAI(prompt: string): Promise<string> {
|
private async callAI(prompt: string): Promise<string> {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
const isLMStudio = config.ollamaUrl.includes('1234') || config.ollamaUrl.includes('v1');
|
const primaryEngine = resolveEngine(config.ollamaUrl);
|
||||||
const apiUrl = isLMStudio ? `${config.ollamaUrl}/v1/chat/completions` : `${config.ollamaUrl}/api/chat`;
|
const engines = primaryEngine === 'lmstudio' ? ['lmstudio', 'ollama'] as const : ['ollama', 'lmstudio'] as const;
|
||||||
|
let lastError: Error | null = null;
|
||||||
|
|
||||||
const payload = {
|
for (const engine of engines) {
|
||||||
model: config.defaultModel,
|
const apiUrl = buildApiUrl(config.ollamaUrl, engine, 'chat');
|
||||||
messages: [{ role: 'user', content: prompt }],
|
const payload = engine === 'lmstudio'
|
||||||
stream: false
|
? {
|
||||||
};
|
model: config.defaultModel,
|
||||||
|
messages: [{ role: 'user', content: prompt }],
|
||||||
|
stream: false
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
model: config.defaultModel,
|
||||||
|
messages: [{ role: 'user', content: prompt }],
|
||||||
|
stream: false
|
||||||
|
};
|
||||||
|
|
||||||
const res = await fetch(apiUrl, {
|
try {
|
||||||
method: 'POST',
|
logInfo('Bridge AI request started.', { engine, apiUrl, model: config.defaultModel });
|
||||||
headers: { 'Content-Type': 'application/json' },
|
const res = await fetch(apiUrl, {
|
||||||
body: JSON.stringify(payload),
|
method: 'POST',
|
||||||
signal: AbortSignal.timeout(config.timeout)
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
body: JSON.stringify(payload),
|
||||||
|
signal: AbortSignal.timeout(config.timeout)
|
||||||
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
const rawText = await res.text();
|
||||||
throw new Error(`Bridge AI call failed: ${res.status}`);
|
if (!res.ok) {
|
||||||
|
lastError = new Error(`Bridge AI call failed: ${res.status} ${summarizeText(rawText, 250)}`);
|
||||||
|
logError('Bridge AI request returned non-OK status.', { engine, apiUrl, status: res.status, body: summarizeText(rawText, 500) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = rawText ? JSON.parse(rawText) as any : {};
|
||||||
|
const content = engine === 'lmstudio'
|
||||||
|
? (data.choices?.[0]?.message?.content || '')
|
||||||
|
: (data.message?.content || data.response || '');
|
||||||
|
|
||||||
|
logInfo('Bridge AI request succeeded.', { engine, apiUrl, responsePreview: summarizeText(content, 200) });
|
||||||
|
return content;
|
||||||
|
} catch (error: any) {
|
||||||
|
lastError = error instanceof Error ? error : new Error(String(error));
|
||||||
|
logError('Bridge AI request failed.', { engine, apiUrl, error: lastError.message });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json() as any;
|
throw lastError || new Error('Bridge AI call failed.');
|
||||||
return isLMStudio ? (data.choices?.[0]?.message?.content || '') : (data.message?.content || '');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-8
@@ -7,7 +7,10 @@ import {
|
|||||||
_getBrainDir,
|
_getBrainDir,
|
||||||
_isBrainDirExplicitlySet,
|
_isBrainDirExplicitlySet,
|
||||||
findBrainFiles,
|
findBrainFiles,
|
||||||
SYSTEM_PROMPT
|
SYSTEM_PROMPT,
|
||||||
|
buildApiUrl,
|
||||||
|
logError,
|
||||||
|
logInfo
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { AgentExecutor } from './agent';
|
import { AgentExecutor } from './agent';
|
||||||
import { BridgeServer } from './bridge';
|
import { BridgeServer } from './bridge';
|
||||||
@@ -17,7 +20,7 @@ import { SidebarChatProvider } from './sidebarProvider';
|
|||||||
* G1nation Extension Entry Point
|
* G1nation Extension Entry Point
|
||||||
*/
|
*/
|
||||||
export async function activate(context: vscode.ExtensionContext) {
|
export async function activate(context: vscode.ExtensionContext) {
|
||||||
console.log('G1nation extension activated.');
|
logInfo('Connect AI extension activated.');
|
||||||
|
|
||||||
// 1. Ensure Brain Directory
|
// 1. Ensure Brain Directory
|
||||||
await _ensureBrainDir(context);
|
await _ensureBrainDir(context);
|
||||||
@@ -35,9 +38,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
const bridge = new BridgeServer(provider);
|
const bridge = new BridgeServer(provider);
|
||||||
try {
|
try {
|
||||||
bridge.start();
|
bridge.start();
|
||||||
console.log('G1nation Bridge Server started on port 4825');
|
logInfo('Bridge server started on port 4825.');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to start Bridge Server:', err);
|
logError('Failed to start bridge server.', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Register Core Commands
|
// 5. Register Core Commands
|
||||||
@@ -74,16 +77,17 @@ async function runInitialSetup(context: vscode.ExtensionContext) {
|
|||||||
let modelName = '';
|
let modelName = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('http://127.0.0.1:1234/v1/models', { signal: AbortSignal.timeout(2000) });
|
const res = await fetch(buildApiUrl('http://127.0.0.1:1234', 'lmstudio', 'models'), { signal: AbortSignal.timeout(2000) });
|
||||||
const data = await res.json() as any;
|
const data = await res.json() as any;
|
||||||
if (data?.data?.length > 0) {
|
if (data?.data?.length > 0) {
|
||||||
engineName = 'LM Studio';
|
engineName = 'LM Studio';
|
||||||
modelName = data.data[0].id;
|
modelName = data.data[0].id;
|
||||||
await vscode.workspace.getConfiguration('g1nation').update('ollamaUrl', 'http://127.0.0.1:1234', vscode.ConfigurationTarget.Global);
|
await vscode.workspace.getConfiguration('g1nation').update('ollamaUrl', 'http://127.0.0.1:1234', vscode.ConfigurationTarget.Global);
|
||||||
await vscode.workspace.getConfiguration('g1nation').update('defaultModel', modelName, vscode.ConfigurationTarget.Global);
|
await vscode.workspace.getConfiguration('g1nation').update('defaultModel', modelName, vscode.ConfigurationTarget.Global);
|
||||||
|
logInfo('Initial setup detected LM Studio.', { modelName });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Setup] LM Studio not found:', err);
|
logInfo('Initial setup could not reach LM Studio.', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!engineName) {
|
if (!engineName) {
|
||||||
@@ -95,9 +99,10 @@ async function runInitialSetup(context: vscode.ExtensionContext) {
|
|||||||
modelName = data.models[0].name;
|
modelName = data.models[0].name;
|
||||||
await vscode.workspace.getConfiguration('g1nation').update('ollamaUrl', 'http://127.0.0.1:11434', vscode.ConfigurationTarget.Global);
|
await vscode.workspace.getConfiguration('g1nation').update('ollamaUrl', 'http://127.0.0.1:11434', vscode.ConfigurationTarget.Global);
|
||||||
await vscode.workspace.getConfiguration('g1nation').update('defaultModel', modelName, vscode.ConfigurationTarget.Global);
|
await vscode.workspace.getConfiguration('g1nation').update('defaultModel', modelName, vscode.ConfigurationTarget.Global);
|
||||||
|
logInfo('Initial setup detected Ollama.', { modelName });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Setup] Ollama not found:', err);
|
logInfo('Initial setup could not reach Ollama.', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +111,7 @@ async function runInitialSetup(context: vscode.ExtensionContext) {
|
|||||||
vscode.window.showInformationMessage(`Setup Complete: ${engineName} detected with model ${modelName}`);
|
vscode.window.showInformationMessage(`Setup Complete: ${engineName} detected with model ${modelName}`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[Setup] Initial setup failed:', e);
|
logError('Initial setup failed.', e);
|
||||||
context.globalState.update('setupComplete', true);
|
context.globalState.update('setupComplete', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-14
@@ -4,6 +4,11 @@ import {
|
|||||||
getConfig,
|
getConfig,
|
||||||
_getBrainDir,
|
_getBrainDir,
|
||||||
findBrainFiles,
|
findBrainFiles,
|
||||||
|
buildApiUrl,
|
||||||
|
logError,
|
||||||
|
logInfo,
|
||||||
|
resolveEngine,
|
||||||
|
summarizeText
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { AgentExecutor } from './agent';
|
import { AgentExecutor } from './agent';
|
||||||
import { BridgeInterface } from './bridge';
|
import { BridgeInterface } from './bridge';
|
||||||
@@ -228,6 +233,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
visionContent: files // Agent seems to handle files via visionContent
|
visionContent: files // Agent seems to handle files via visionContent
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
logError('Prompt handling failed in sidebar provider.', { error: error?.message || String(error), promptPreview: summarizeText(value || '', 200) });
|
||||||
this._view.webview.postMessage({ type: 'error', value: error.message });
|
this._view.webview.postMessage({ type: 'error', value: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,22 +246,32 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
let defaultModel = config.defaultModel;
|
let defaultModel = config.defaultModel;
|
||||||
let models: string[] = [];
|
let models: string[] = [];
|
||||||
|
|
||||||
if (url.includes('1234') || url.includes('v1')) {
|
const primaryEngine = resolveEngine(url);
|
||||||
|
const engines = primaryEngine === 'lmstudio' ? ['lmstudio', 'ollama'] as const : ['ollama', 'lmstudio'] as const;
|
||||||
|
|
||||||
|
for (const engine of engines) {
|
||||||
|
const modelsUrl = buildApiUrl(url, engine, 'models');
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${url}/v1/models`, { signal: AbortSignal.timeout(5000) });
|
logInfo('Model discovery started.', { engine, modelsUrl });
|
||||||
if (res.ok) {
|
const res = await fetch(modelsUrl, { signal: AbortSignal.timeout(5000) });
|
||||||
const data = await res.json() as any;
|
const rawText = await res.text();
|
||||||
models = data.data.map((m: any) => m.id);
|
if (!res.ok) {
|
||||||
|
logError('Model discovery returned non-OK status.', { engine, modelsUrl, status: res.status, body: summarizeText(rawText, 300) });
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
} catch (e) { console.error("[G1] LM Studio models fetch failed:", e); }
|
|
||||||
} else {
|
const data = rawText ? JSON.parse(rawText) as any : {};
|
||||||
try {
|
models = engine === 'lmstudio'
|
||||||
const res = await fetch(`${url}/api/tags`, { signal: AbortSignal.timeout(5000) });
|
? (data.data || []).map((m: any) => m.id)
|
||||||
if (res.ok) {
|
: (data.models || []).map((m: any) => m.name);
|
||||||
const data = await res.json() as any;
|
|
||||||
models = data.models.map((m: any) => m.name);
|
if (models.length > 0) {
|
||||||
|
logInfo('Model discovery succeeded.', { engine, count: models.length, modelsPreview: models.slice(0, 5) });
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} catch (e) { console.error("[G1] Ollama models fetch failed:", e); }
|
} catch (e: any) {
|
||||||
|
logError('Model discovery failed.', { engine, modelsUrl, error: e?.message || String(e) });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (models.length === 0) {
|
if (models.length === 0) {
|
||||||
@@ -278,6 +294,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
|
|
||||||
this._view.webview.postMessage({ type: 'modelsList', value: models });
|
this._view.webview.postMessage({ type: 'modelsList', value: models });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
logError('Model list update failed.', err);
|
||||||
this._view.webview.postMessage({ type: 'modelsList', value: [getConfig().defaultModel] });
|
this._view.webview.postMessage({ type: 'modelsList', value: [getConfig().defaultModel] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -513,7 +530,7 @@ window.addEventListener('message',e=>{const msg=e.data;switch(msg.type){
|
|||||||
case 'restoreHistory':
|
case 'restoreHistory':
|
||||||
chat.innerHTML='';
|
chat.innerHTML='';
|
||||||
document.body.classList.remove('init');
|
document.body.classList.remove('init');
|
||||||
msg.value.forEach(m => addMsg(m.content, m.role === 'assistant' ? 'ai' : 'user'));
|
msg.value.filter(m => !m.internal).forEach(m => addMsg(m.content, m.role === 'assistant' ? 'ai' : 'user'));
|
||||||
break;
|
break;
|
||||||
case 'sessionList':
|
case 'sessionList':
|
||||||
historyList.innerHTML='';
|
historyList.innerHTML='';
|
||||||
|
|||||||
@@ -24,6 +24,82 @@ export function getConfig() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EngineKind = 'lmstudio' | 'ollama';
|
||||||
|
|
||||||
|
const outputChannel = vscode.window.createOutputChannel('Connect AI');
|
||||||
|
|
||||||
|
function timestamp() {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyMeta(meta: unknown): string {
|
||||||
|
if (meta === undefined) return '';
|
||||||
|
if (typeof meta === 'string') return meta;
|
||||||
|
if (meta instanceof Error) return `${meta.name}: ${meta.message}\n${meta.stack || ''}`;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(meta, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(meta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendLog(level: 'INFO' | 'WARN' | 'ERROR', message: string, meta?: unknown) {
|
||||||
|
const suffix = meta === undefined ? '' : `\n${stringifyMeta(meta)}`;
|
||||||
|
outputChannel.appendLine(`[${timestamp()}] [${level}] ${message}${suffix}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logInfo(message: string, meta?: unknown) {
|
||||||
|
appendLog('INFO', message, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logWarn(message: string, meta?: unknown) {
|
||||||
|
appendLog('WARN', message, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logError(message: string, meta?: unknown) {
|
||||||
|
appendLog('ERROR', message, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeBaseUrl(rawUrl: string): string {
|
||||||
|
const trimmed = rawUrl.trim().replace(/\/+$/, '');
|
||||||
|
if (!trimmed) {
|
||||||
|
return 'http://127.0.0.1:11434';
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveEngine(baseUrl: string): EngineKind {
|
||||||
|
const normalized = normalizeBaseUrl(baseUrl);
|
||||||
|
try {
|
||||||
|
const parsed = new URL(normalized);
|
||||||
|
if (parsed.pathname.endsWith('/v1') || parsed.port === '1234') return 'lmstudio';
|
||||||
|
if (parsed.pathname.endsWith('/api') || parsed.port === '11434') return 'ollama';
|
||||||
|
} catch {
|
||||||
|
if (normalized.includes('/v1') || normalized.includes(':1234')) return 'lmstudio';
|
||||||
|
}
|
||||||
|
return 'ollama';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiUrl(baseUrl: string, engine: EngineKind, endpoint: 'models' | 'chat'): string {
|
||||||
|
const normalized = normalizeBaseUrl(baseUrl);
|
||||||
|
if (engine === 'lmstudio') {
|
||||||
|
if (normalized.endsWith('/v1')) {
|
||||||
|
return endpoint === 'models' ? `${normalized}/models` : `${normalized}/chat/completions`;
|
||||||
|
}
|
||||||
|
return endpoint === 'models' ? `${normalized}/v1/models` : `${normalized}/v1/chat/completions`;
|
||||||
|
}
|
||||||
|
if (normalized.endsWith('/api')) {
|
||||||
|
return endpoint === 'models' ? `${normalized}/tags` : `${normalized}/chat`;
|
||||||
|
}
|
||||||
|
return endpoint === 'models' ? `${normalized}/api/tags` : `${normalized}/api/chat`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function summarizeText(text: string, maxLength: number = 400): string {
|
||||||
|
const normalized = text.replace(/\s+/g, ' ').trim();
|
||||||
|
if (normalized.length <= maxLength) return normalized;
|
||||||
|
return `${normalized.slice(0, maxLength)}...`;
|
||||||
|
}
|
||||||
|
|
||||||
export function shouldAutoPushBrain(): boolean {
|
export function shouldAutoPushBrain(): boolean {
|
||||||
const cfg = vscode.workspace.getConfiguration('g1nation');
|
const cfg = vscode.workspace.getConfiguration('g1nation');
|
||||||
return cfg.get<boolean>('autoPushBrain', false);
|
return cfg.get<boolean>('autoPushBrain', false);
|
||||||
|
|||||||
Reference in New Issue
Block a user