From 7430c91177f4f4f39006db4ce5fabf2b723c064c Mon Sep 17 00:00:00 2001 From: Wonseok Jung Date: Thu, 30 Apr 2026 00:31:27 +0900 Subject: [PATCH] fix: v2.13.0 - stability fixes (model persistence & agent abort logic) --- package.json | 2 +- src/agent.ts | 17 ++++++++-- src/agents/factory.ts | 77 ++++++++++++++++++++++++++++++------------- 3 files changed, 69 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 302176b..f08a6fc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "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.", - "version": "2.12.0", + "version": "2.13.0", "publisher": "connectailab", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/agent.ts b/src/agent.ts index 42a53fb..2d90e5c 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -534,6 +534,10 @@ export class AgentExecutor { options: any ) { 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.webview.postMessage({ type: 'streamStart' }); @@ -549,19 +553,23 @@ export class AgentExecutor { const brainContext = `Brain: ${activeBrain.name}, Files: ${brainFiles.length}`; // --- Phase 1: Planner --- + if (signal.aborted) return; 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` }); // --- Phase 2: Researcher --- + if (signal.aborted) return; 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` }); // --- Phase 3: Writer --- + if (signal.aborted) return; 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: 'streamEnd' }); @@ -575,6 +583,9 @@ export class AgentExecutor { const friendly = ErrorTranslator.translate(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) this.webview.postMessage({ type: 'error', diff --git a/src/agents/factory.ts b/src/agents/factory.ts index faa1bea..a526a9c 100644 --- a/src/agents/factory.ts +++ b/src/agents/factory.ts @@ -4,52 +4,83 @@ import { getConfig } from '../config'; export abstract class BaseAgent { constructor(protected readonly modelName: string) {} - protected async callLLM(persona: string, prompt: string): Promise { + protected async callLLM(persona: string, prompt: string, signal?: AbortSignal): Promise { const { ollamaUrl } = getConfig(); const messages = [ { role: 'system', content: persona }, { role: 'user', content: prompt } ]; - // API 호출 로직 (Streaming 생략하고 결과만 반환하는 헬퍼) - const response = await fetch(`${ollamaUrl}/api/chat`, { - method: 'POST', - body: JSON.stringify({ - model: this.modelName, - messages, - stream: false, - options: { temperature: 0.3 } - }) - }); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 45000); // Increased to 45s for complex tasks - if (!response.ok) { - throw new Error(`Agent API Error: ${response.statusText}`); + // Combine external signal with local timeout + 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; + abstract execute(input: string, context?: string, signal?: AbortSignal): Promise; +} + +// 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 { private readonly persona = `You are the [Planner Agent]. Analyze the request and output a structured .`; - async execute(input: string, brainContext?: string): Promise { - return this.callLLM(this.persona, `Request: ${input}\nContext: ${brainContext}`); + async execute(input: string, brainContext?: string, signal?: AbortSignal): Promise { + return this.callLLM(this.persona, `Request: ${input}\nContext: ${brainContext}`, signal); } } export class ResearcherAgent extends BaseAgent { private readonly persona = `You are the [Researcher Agent]. Gather facts based on the plan.`; - async execute(input: string, brainContext?: string): Promise { - return this.callLLM(this.persona, `Plan: ${input}\nContext: ${brainContext}`); + async execute(input: string, brainContext?: string, signal?: AbortSignal): Promise { + return this.callLLM(this.persona, `Plan: ${input}\nContext: ${brainContext}`, signal); } } export class WriterAgent extends BaseAgent { private readonly persona = `You are the [Writer Agent]. Synthesize research into a final report.`; - async execute(input: string, originalRequest?: string): Promise { - return this.callLLM(this.persona, `Data: ${input}\nOriginal Request: ${originalRequest}`); + async execute(input: string, originalRequest?: string, signal?: AbortSignal): Promise { + return this.callLLM(this.persona, `Data: ${input}\nOriginal Request: ${originalRequest}`, signal); } }