From 99ddf6a3cf7fc6ffdd479f7f73a3fbdf12a04b29 Mon Sep 17 00:00:00 2001 From: g1nation Date: Thu, 7 May 2026 13:52:38 +0900 Subject: [PATCH] chore: version bump to 2.80.9 and fix bridge/lmstudio issues --- ...d46d2ca2057b05c488be1dcf439166ac5a9a1.json | 2 +- ...9f4f39d2bc368f77456c37b5eef9a94a66b5c.json | 2 +- ...5c7a44d7661af673b24e3f49551a7a2e50280.json | 2 +- ...adc543795e4b427b64540a49c9ab27c7fe213.json | 4 +- ...son => stress_conflict_1778129530743.json} | 22 +-- package.json | 2 +- src/agent.ts | 161 +++++++++++++----- src/bridge.ts | 19 ++- 8 files changed, 152 insertions(+), 62 deletions(-) rename .astra/tests/stress/.astra/missions/{stress_conflict_1778045762177.json => stress_conflict_1778129530743.json} (81%) diff --git a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json index 7dca09c..de5fadd 100644 --- a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json +++ b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json @@ -1,5 +1,5 @@ { "result": "Final report with inconsistencies. This should be long enough to pass validation.", - "createdAt": 1778045762198, + "createdAt": 1778129530764, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json index eaa7b9a..3f94a0d 100644 --- a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json +++ b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json @@ -1,5 +1,5 @@ { "result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", - "createdAt": 1778045762196, + "createdAt": 1778129530762, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json index 89bd095..6d5317b 100644 --- a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json +++ b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json @@ -1,5 +1,5 @@ { "result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", - "createdAt": 1778045762195, + "createdAt": 1778129530759, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json index d0f4658..a772dc8 100644 --- a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json +++ b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json @@ -1,5 +1,5 @@ { - "result": "---\nid: stress_conflict_1778045762177\ndate: 2026-05-06T05:36:02.199Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (16ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (2ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (3ms)\n", - "createdAt": 1778045762200, + "result": "---\nid: stress_conflict_1778129530743\ndate: 2026-05-07T04:52:10.766Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (15ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (3ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (2ms)\n", + "createdAt": 1778129530766, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/missions/stress_conflict_1778045762177.json b/.astra/tests/stress/.astra/missions/stress_conflict_1778129530743.json similarity index 81% rename from .astra/tests/stress/.astra/missions/stress_conflict_1778045762177.json rename to .astra/tests/stress/.astra/missions/stress_conflict_1778129530743.json index d548b68..18c7704 100644 --- a/.astra/tests/stress/.astra/missions/stress_conflict_1778045762177.json +++ b/.astra/tests/stress/.astra/missions/stress_conflict_1778129530743.json @@ -1,8 +1,8 @@ { - "missionId": "stress_conflict_1778045762177", + "missionId": "stress_conflict_1778129530743", "status": "completed", - "startTime": "2026-05-06T05:36:02.177Z", - "totalElapsedMs": 23, + "startTime": "2026-05-07T04:52:10.743Z", + "totalElapsedMs": 24, "results": { "planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", "researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", @@ -16,30 +16,30 @@ { "from": "idle", "to": "planner", - "durationMs": 16, + "durationMs": 15, "message": "전략 수립 중...", - "ts": "2026-05-06T05:36:02.193Z" + "ts": "2026-05-07T04:52:10.758Z" }, { "from": "planner", "to": "researcher", - "durationMs": 2, + "durationMs": 3, "message": "핵심 정보 수집 및 분석 중...", - "ts": "2026-05-06T05:36:02.195Z" + "ts": "2026-05-07T04:52:10.761Z" }, { "from": "researcher", "to": "writer", - "durationMs": 3, + "durationMs": 2, "message": "최종 리포트 작성 및 편집 중...", - "ts": "2026-05-06T05:36:02.198Z" + "ts": "2026-05-07T04:52:10.763Z" }, { "from": "writer", "to": "completed", - "durationMs": 2, + "durationMs": 4, "message": "미션 완료", - "ts": "2026-05-06T05:36:02.200Z" + "ts": "2026-05-07T04:52:10.767Z" } ], "resilienceMetrics": { diff --git a/package.json b/package.json index d27b25b..771f02a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "astra", "displayName": "Astra", "description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.", - "version": "2.80.5", + "version": "2.80.9", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/agent.ts b/src/agent.ts index 67eb25c..4be309f 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -451,35 +451,74 @@ export class AgentExecutor { if (this.isStaleRun(runId)) return; let aiResponseText = ''; - const reader = response.body?.getReader(); - if (!reader) throw new Error("Response body is not readable."); + const body = response.body as any; + if (!body) throw new Error("Response body is null."); - if (loopDepth === 0) this.webview.postMessage({ type: 'streamStart' }); + if (loopDepth === 0) this.webview?.postMessage({ type: 'streamStart' }); let buffer = ''; const decoder = new TextDecoder(); - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - if (this.isStaleRun(runId)) return; - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed || trimmed === 'data: [DONE]') continue; - try { - const raw = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed; - const json = JSON.parse(raw); - const token = engine === 'lmstudio' ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || ''; - if (token) { - aiResponseText += token; - } - } catch (e: any) { - logError('Failed to parse streaming chunk.', { engine, apiUrl, chunk: summarizeText(trimmed, 300), error: e?.message || String(e) }); + const processChunk = (value: any) => { + if (this.isStaleRun(runId)) return false; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed === 'data: [DONE]') continue; + + try { + let raw = trimmed; + if (trimmed.startsWith('data:')) { + raw = trimmed.replace(/^data:\s*/, ''); } + + if (!raw || raw === '[DONE]') continue; + + const json = JSON.parse(raw); + if (json.error) { + const errMsg = typeof json.error === 'string' ? json.error : (json.error.message || JSON.stringify(json.error)); + throw new Error(`AI Engine Error: ${errMsg}`); + } + + let token = ''; + if (json.choices?.[0]) { + const choice = json.choices[0]; + token = choice.delta?.content || choice.message?.content || choice.text || ''; + } else if (json.message?.content) { + token = json.message.content; + } else if (json.response) { + token = json.response; + } + + if (token) { + aiResponseText += token; + if (loopDepth === 0) { + this.webview?.postMessage({ type: 'streamUpdate', value: token }); + } + } + } catch (e: any) { + // Silent fail for non-JSON lines unless it's an AI Engine Error + if (e.message.startsWith('AI Engine Error:')) throw e; + } + } + return true; + }; + + try { + if (typeof body[Symbol.asyncIterator] === 'function') { + for await (const chunk of body) { + if (!processChunk(chunk)) break; + } + } else { + const reader = body.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (!processChunk(value)) break; } } } catch (err: any) { @@ -495,11 +534,30 @@ export class AgentExecutor { if (buffer.trim() && buffer.trim() !== 'data: [DONE]') { try { const trimmed = buffer.trim(); - const raw = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed; - const json = JSON.parse(raw); - const token = engine === 'lmstudio' ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || ''; - if (token) { - aiResponseText += token; + let raw = trimmed; + if (trimmed.startsWith('data:')) { + raw = trimmed.replace(/^data:\s*/, ''); + } + + if (raw && raw !== '[DONE]') { + const json = JSON.parse(raw); + if (json.error) { + const errMsg = typeof json.error === 'string' ? json.error : (json.error.message || JSON.stringify(json.error)); + throw new Error(`AI Engine Error: ${errMsg}`); + } + let token = ''; + if (json.choices?.[0]) { + const choice = json.choices[0]; + token = choice.delta?.content || choice.message?.content || choice.text || ''; + } else if (json.message?.content) { + token = json.message.content; + } else if (json.response) { + token = json.response; + } + + if (token) { + aiResponseText += token; + } } } catch (e: any) { logError('Failed to parse final streaming buffer.', { engine, apiUrl, buffer: summarizeText(buffer, 300), error: e?.message || String(e) }); @@ -556,24 +614,36 @@ export class AgentExecutor { */ const finalAssistantContent = assistantContent; + const assistantMessage: ChatMessage = { role: 'assistant', content: finalAssistantContent, internal: false, rationale }; + this.chatHistory.push(assistantMessage); + this.emitHistoryChanged(); + this.statusBarManager.updateStatus(AgentStatus.Executing); const report = await this.executeActions(aiResponseText, rootPath, activeBrain); if (!assistantContent.trim() && report.length === 0) { - logError('Model returned an empty response without actions.', { model: actualModel, engine, apiUrl, loopDepth }); + const totalChars2 = messagesForRequest.reduce((acc, m) => acc + String(m.content || '').length, 0); + const estimatedTokens2 = Math.ceil(totalChars2 / 4); + const isContextOverflow = estimatedTokens2 > 5000; + logError('Model returned an empty response without actions.', { model: actualModel, engine, apiUrl, loopDepth, estimatedTokens: estimatedTokens2 }); this.webview.postMessage({ type: 'error', value: [ 'AI engine returned an empty response.', - `Engine: ${engine}`, - `Model: ${actualModel}`, - 'The request reached the local LLM server, but no usable content was returned. Try another model, restart the local server, or reduce the prompt/context size.' + `Engine: ${engine} | Model: ${actualModel}`, + isContextOverflow + ? `Context overflow: ~${estimatedTokens2.toLocaleString()} tokens estimated. This model likely has a smaller context window.` + : 'The request reached the LLM server, but no content was returned.', + '', + '**해결 방법:**', + isContextOverflow + ? '1. Brain 비활성화 후 재시도 2. 더 큰 모델(7B+) 사용 3. 대화 기록 초기화 후 재시도' + : '1. LM Studio에서 해당 모델이 로드되어 있는지 확인 2. 모델 재시작 후 재시도 3. 다른 모델로 전환' ].join('\n') }); return; } if (report.length > 0) { - this.emitHistoryChanged(); logInfo('Agent actions executed.', { loopDepth: loopDepth + 1, report }); // Continue loop if needed @@ -590,7 +660,7 @@ export class AgentExecutor { logInfo('Autonomous loop continuing after actions.', { loopDepth: loopDepth + 1, actions: report }); // Explicitly tell the AI to look at the results and continue - const continuationPrompt = "The requested local action has been executed. Use the action result messages already in the conversation to answer the user's original request directly, in the user's language. Do not say you are waiting for the next instruction."; + const continuationPrompt = `The requested local action has been executed.\nAction report:\n${report.join('\n')}\nUse the action result messages already in the conversation to answer the user's original request directly, in the user's language. Do not say you are waiting for the next instruction.`; this.webview.postMessage({ type: 'autoContinue', value: `자료를 확인하고 답변을 정리하는 중입니다... (${loopDepth + 1}/${config.maxAutoSteps})` }); await new Promise(r => setTimeout(r, 800)); @@ -600,9 +670,6 @@ export class AgentExecutor { return; } - const assistantMessage: ChatMessage = { role: 'assistant', content: finalAssistantContent, internal: false, rationale }; - this.chatHistory.push(assistantMessage); - this.emitHistoryChanged(); this.statusBarManager.updateStatus(AgentStatus.Success); this.webview.postMessage({ type: 'streamChunk', value: finalAssistantContent }); @@ -1953,14 +2020,29 @@ export class AgentExecutor { for (const candidateModel of modelCandidates) { for (const variant of messageVariants) { + // LM Studio: context_length를 명시적으로 제한하여 컨텍스트 초과 방지 + // 총 메시지 토큰 추정: 문자 수 / 4 (rough estimate) + const totalChars = variant.messages.reduce((acc, m) => acc + String(m.content || '').length, 0); + const estimatedTokens = Math.ceil(totalChars / 4); + // LM Studio 소형 모델(4B~8B)은 4096~8192 context 제한 + // 컨텍스트 초과 시 max_tokens을 줄여서 모델이 응답할 공간 확보 + const lmStudioMaxTokens = Math.max(512, Math.min(4096, 8192 - estimatedTokens)); const streamBody = { model: candidateModel, messages: variant.messages, stream: true, ...(engine === 'lmstudio' - ? { max_tokens: 4096, temperature } + ? { + max_tokens: lmStudioMaxTokens, + temperature, + // LM Studio: context_length로 컨텍스트 창 명시 설정 + context_length: 8192 + } : { options: { num_ctx: 32768, num_predict: 4096, temperature } }), }; + if (engine === 'lmstudio' && estimatedTokens > 6000) { + logError('LM Studio context may be too large for small models.', { estimatedTokens, lmStudioMaxTokens, model: candidateModel }); + } try { logInfo('AI streaming request started.', { @@ -1981,8 +2063,7 @@ export class AgentExecutor { 'Connection': 'keep-alive' }, body: JSON.stringify(streamBody), - signal: this.abortController?.signal, - keepalive: true + signal: this.abortController?.signal }); if (!response.ok) { diff --git a/src/bridge.ts b/src/bridge.ts index 8c6e20c..62a39a7 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -39,7 +39,7 @@ export class BridgeServer { } public start(port: number = 4825) { - this.server = http.createServer((req, res) => { + const server = http.createServer((req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); @@ -70,15 +70,24 @@ export class BridgeServer { } }); - this.server.on('error', (err: any) => { + // once() 사용: 중복 에러 이벤트 방지 + server.once('error', (err: any) => { if (err.code === 'EADDRINUSE') { - logError(`🚫 Bridge Port ${port} in use. Connection with EZER/A.U might fail.`); + logInfo(`Bridge Port ${port} already in use. Trying port ${port + 1}...`); + // 기존 서버 참조 정리 후 다음 포트 시도 + server.close(); + if (this.server === server) { + this.server = null; + } + this.start(port + 1); } else { - logError(`Bridge server error:`, err); + logError(`Bridge server error on port ${port}:`, err); } }); - this.server.listen(port, '127.0.0.1', () => { + // 성공 시 서버 참조 저장 + server.listen(port, '127.0.0.1', () => { + this.server = server; logInfo(`Bridge server active on 127.0.0.1:${port}.`); }); }