fix: v2.13.0 - stability fixes (model persistence & agent abort logic)
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.12.0",
|
"version": "2.13.0",
|
||||||
"publisher": "connectailab",
|
"publisher": "connectailab",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
|
|||||||
+14
-3
@@ -534,6 +534,10 @@ export class AgentExecutor {
|
|||||||
options: any
|
options: any
|
||||||
) {
|
) {
|
||||||
if (!this.webview) return;
|
if (!this.webview) return;
|
||||||
|
this.stop(); // Abort any previous run
|
||||||
|
this.abortController = new AbortController();
|
||||||
|
const signal = this.abortController.signal;
|
||||||
|
|
||||||
this.statusBarManager.updateStatus(AgentStatus.Thinking, 'Multi-Agent Workflow Started');
|
this.statusBarManager.updateStatus(AgentStatus.Thinking, 'Multi-Agent Workflow Started');
|
||||||
this.webview.postMessage({ type: 'streamStart' });
|
this.webview.postMessage({ type: 'streamStart' });
|
||||||
|
|
||||||
@@ -549,19 +553,23 @@ export class AgentExecutor {
|
|||||||
const brainContext = `Brain: ${activeBrain.name}, Files: ${brainFiles.length}`;
|
const brainContext = `Brain: ${activeBrain.name}, Files: ${brainFiles.length}`;
|
||||||
|
|
||||||
// --- Phase 1: Planner ---
|
// --- Phase 1: Planner ---
|
||||||
|
if (signal.aborted) return;
|
||||||
this.webview.postMessage({ type: 'autoContinue', value: 'Planner: 전략 수립 중...' });
|
this.webview.postMessage({ type: 'autoContinue', value: 'Planner: 전략 수립 중...' });
|
||||||
const plan = await planner.execute(prompt, brainContext);
|
const plan = await planner.execute(prompt, brainContext, signal);
|
||||||
this.webview.postMessage({ type: 'streamChunk', value: `\n\n### 📝 작업 계획 (Execution Plan)\n${plan}\n\n` });
|
this.webview.postMessage({ type: 'streamChunk', value: `\n\n### 📝 작업 계획 (Execution Plan)\n${plan}\n\n` });
|
||||||
|
|
||||||
// --- Phase 2: Researcher ---
|
// --- Phase 2: Researcher ---
|
||||||
|
if (signal.aborted) return;
|
||||||
this.webview.postMessage({ type: 'autoContinue', value: 'Researcher: 지식 검색 중...' });
|
this.webview.postMessage({ type: 'autoContinue', value: 'Researcher: 지식 검색 중...' });
|
||||||
const research = await researcher.execute(plan, brainContext);
|
const research = await researcher.execute(plan, brainContext, signal);
|
||||||
this.webview.postMessage({ type: 'streamChunk', value: `\n\n### 🔍 분석 결과 (Research Findings)\n*(정보 수집 및 정제 완료)*\n\n` });
|
this.webview.postMessage({ type: 'streamChunk', value: `\n\n### 🔍 분석 결과 (Research Findings)\n*(정보 수집 및 정제 완료)*\n\n` });
|
||||||
|
|
||||||
// --- Phase 3: Writer ---
|
// --- Phase 3: Writer ---
|
||||||
|
if (signal.aborted) return;
|
||||||
this.webview.postMessage({ type: 'autoContinue', value: 'Writer: 보고서 작성 중...' });
|
this.webview.postMessage({ type: 'autoContinue', value: 'Writer: 보고서 작성 중...' });
|
||||||
const finalReport = await writer.execute(research, prompt);
|
const finalReport = await writer.execute(research, prompt, signal);
|
||||||
|
|
||||||
|
if (signal.aborted) return;
|
||||||
this.webview.postMessage({ type: 'streamChunk', value: `\n\n--- \n\n${finalReport}` });
|
this.webview.postMessage({ type: 'streamChunk', value: `\n\n--- \n\n${finalReport}` });
|
||||||
this.webview.postMessage({ type: 'streamEnd' });
|
this.webview.postMessage({ type: 'streamEnd' });
|
||||||
|
|
||||||
@@ -575,6 +583,9 @@ export class AgentExecutor {
|
|||||||
const friendly = ErrorTranslator.translate(error);
|
const friendly = ErrorTranslator.translate(error);
|
||||||
logError('Workflow failed', error);
|
logError('Workflow failed', error);
|
||||||
|
|
||||||
|
// Clear autoContinue state by sending empty value or specific type
|
||||||
|
this.webview.postMessage({ type: 'autoContinue', value: '' });
|
||||||
|
|
||||||
// Format error using guideline-compliant UI (Red color scheme)
|
// Format error using guideline-compliant UI (Red color scheme)
|
||||||
this.webview.postMessage({
|
this.webview.postMessage({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
|||||||
+54
-23
@@ -4,52 +4,83 @@ import { getConfig } from '../config';
|
|||||||
export abstract class BaseAgent {
|
export abstract class BaseAgent {
|
||||||
constructor(protected readonly modelName: string) {}
|
constructor(protected readonly modelName: string) {}
|
||||||
|
|
||||||
protected async callLLM(persona: string, prompt: string): Promise<string> {
|
protected async callLLM(persona: string, prompt: string, signal?: AbortSignal): Promise<string> {
|
||||||
const { ollamaUrl } = getConfig();
|
const { ollamaUrl } = getConfig();
|
||||||
const messages = [
|
const messages = [
|
||||||
{ role: 'system', content: persona },
|
{ role: 'system', content: persona },
|
||||||
{ role: 'user', content: prompt }
|
{ role: 'user', content: prompt }
|
||||||
];
|
];
|
||||||
|
|
||||||
// API 호출 로직 (Streaming 생략하고 결과만 반환하는 헬퍼)
|
const controller = new AbortController();
|
||||||
const response = await fetch(`${ollamaUrl}/api/chat`, {
|
const timeoutId = setTimeout(() => controller.abort(), 45000); // Increased to 45s for complex tasks
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: this.modelName,
|
|
||||||
messages,
|
|
||||||
stream: false,
|
|
||||||
options: { temperature: 0.3 }
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
// Combine external signal with local timeout
|
||||||
throw new Error(`Agent API Error: ${response.statusText}`);
|
const combinedSignal = signal ?
|
||||||
|
anySignal([signal, controller.signal]) :
|
||||||
|
controller.signal;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${ollamaUrl}/api/chat`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: this.modelName,
|
||||||
|
messages,
|
||||||
|
stream: false,
|
||||||
|
options: { temperature: 0.3 }
|
||||||
|
}),
|
||||||
|
signal: combinedSignal
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Agent API Error: ${response.statusText} (${response.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json() as any;
|
||||||
|
return data.message?.content || data.choices?.[0]?.message?.content || '';
|
||||||
|
} catch (error: any) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
throw new Error('Agent request was cancelled or timed out.');
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json() as any;
|
|
||||||
return data.message?.content || data.choices?.[0]?.message?.content || '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract execute(input: string, context?: string): Promise<string>;
|
abstract execute(input: string, context?: string, signal?: AbortSignal): Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to combine signals (since AbortSignal.any is not always available in older Node)
|
||||||
|
function anySignal(signals: AbortSignal[]): AbortSignal {
|
||||||
|
const controller = new AbortController();
|
||||||
|
for (const signal of signals) {
|
||||||
|
if (signal.aborted) {
|
||||||
|
controller.abort();
|
||||||
|
return signal;
|
||||||
|
}
|
||||||
|
signal.addEventListener('abort', () => controller.abort(), { once: true });
|
||||||
|
}
|
||||||
|
return controller.signal;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PlannerAgent extends BaseAgent {
|
export class PlannerAgent extends BaseAgent {
|
||||||
private readonly persona = `You are the [Planner Agent]. Analyze the request and output a structured <plan>.`;
|
private readonly persona = `You are the [Planner Agent]. Analyze the request and output a structured <plan>.`;
|
||||||
async execute(input: string, brainContext?: string): Promise<string> {
|
async execute(input: string, brainContext?: string, signal?: AbortSignal): Promise<string> {
|
||||||
return this.callLLM(this.persona, `Request: ${input}\nContext: ${brainContext}`);
|
return this.callLLM(this.persona, `Request: ${input}\nContext: ${brainContext}`, signal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ResearcherAgent extends BaseAgent {
|
export class ResearcherAgent extends BaseAgent {
|
||||||
private readonly persona = `You are the [Researcher Agent]. Gather facts based on the plan.`;
|
private readonly persona = `You are the [Researcher Agent]. Gather facts based on the plan.`;
|
||||||
async execute(input: string, brainContext?: string): Promise<string> {
|
async execute(input: string, brainContext?: string, signal?: AbortSignal): Promise<string> {
|
||||||
return this.callLLM(this.persona, `Plan: ${input}\nContext: ${brainContext}`);
|
return this.callLLM(this.persona, `Plan: ${input}\nContext: ${brainContext}`, signal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WriterAgent extends BaseAgent {
|
export class WriterAgent extends BaseAgent {
|
||||||
private readonly persona = `You are the [Writer Agent]. Synthesize research into a final report.`;
|
private readonly persona = `You are the [Writer Agent]. Synthesize research into a final report.`;
|
||||||
async execute(input: string, originalRequest?: string): Promise<string> {
|
async execute(input: string, originalRequest?: string, signal?: AbortSignal): Promise<string> {
|
||||||
return this.callLLM(this.persona, `Data: ${input}\nOriginal Request: ${originalRequest}`);
|
return this.callLLM(this.persona, `Data: ${input}\nOriginal Request: ${originalRequest}`, signal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user