From eb36cec050c4139e76f3b7c531ba6cf22e0943e8 Mon Sep 17 00:00:00 2001 From: g1nation Date: Wed, 13 May 2026 00:15:45 +0900 Subject: [PATCH] chore: version up to 2.80.38 and package with refined recovery --- ...d46d2ca2057b05c488be1dcf439166ac5a9a1.json | 2 +- ...9f4f39d2bc368f77456c37b5eef9a94a66b5c.json | 2 +- ...5c7a44d7661af673b24e3f49551a7a2e50280.json | 2 +- ...adc543795e4b427b64540a49c9ab27c7fe213.json | 4 +- ...son => stress_conflict_1778598898506.json} | 20 ++++---- docs/records/ConnectAI/chronicle.config.json | 4 +- ...고-부족한-부분이나-개선이_implementation-2.md | 22 ++++++++ ...고-부족한-부분이나-개선이_implementation.md | 22 ++++++++ docs/records/ConnectAI/timeline.md | 6 +++ package.json | 2 +- src/agent.ts | 50 ++++++++++++------- src/core/responseRecovery.ts | 44 +++++++++++++--- src/lmstudio/streamer.ts | 4 +- src/sidebarProvider.ts | 39 +++++++++------ tests/responseRecovery.test.ts | 41 +++++++++++++-- 15 files changed, 202 insertions(+), 62 deletions(-) rename .astra/tests/stress/.astra/missions/{stress_conflict_1778597639274.json => stress_conflict_1778598898506.json} (80%) create mode 100644 docs/records/ConnectAI/development/2026-05-12_volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이_implementation-2.md create mode 100644 docs/records/ConnectAI/development/2026-05-12_volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이_implementation.md diff --git a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json index 9382dd9..ef8fc37 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": 1778597639298, + "createdAt": 1778598898519, "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 412413e..ce9a568 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": 1778597639290, + "createdAt": 1778598898518, "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 5e801f7..e51b886 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": 1778597639286, + "createdAt": 1778598898517, "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 6dc943e..3ca3153 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_1778597639274\ndate: 2026-05-12T14:53:59.302Z\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]** 전략 수립 중... (11ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (1ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (8ms)\n", - "createdAt": 1778597639302, + "result": "---\nid: stress_conflict_1778598898506\ndate: 2026-05-12T15:14:58.520Z\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]** 전략 수립 중... (10ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (1ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (2ms)\n", + "createdAt": 1778598898520, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/missions/stress_conflict_1778597639274.json b/.astra/tests/stress/.astra/missions/stress_conflict_1778598898506.json similarity index 80% rename from .astra/tests/stress/.astra/missions/stress_conflict_1778597639274.json rename to .astra/tests/stress/.astra/missions/stress_conflict_1778598898506.json index 5110b18..61ebd82 100644 --- a/.astra/tests/stress/.astra/missions/stress_conflict_1778597639274.json +++ b/.astra/tests/stress/.astra/missions/stress_conflict_1778598898506.json @@ -1,8 +1,8 @@ { - "missionId": "stress_conflict_1778597639274", + "missionId": "stress_conflict_1778598898506", "status": "completed", - "startTime": "2026-05-12T14:53:59.274Z", - "totalElapsedMs": 28, + "startTime": "2026-05-12T15:14:58.506Z", + "totalElapsedMs": 14, "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": 11, + "durationMs": 10, "message": "전략 수립 중...", - "ts": "2026-05-12T14:53:59.285Z" + "ts": "2026-05-12T15:14:58.516Z" }, { "from": "planner", "to": "researcher", "durationMs": 1, "message": "핵심 정보 수집 및 분석 중...", - "ts": "2026-05-12T14:53:59.286Z" + "ts": "2026-05-12T15:14:58.517Z" }, { "from": "researcher", "to": "writer", - "durationMs": 8, + "durationMs": 2, "message": "최종 리포트 작성 및 편집 중...", - "ts": "2026-05-12T14:53:59.294Z" + "ts": "2026-05-12T15:14:58.519Z" }, { "from": "writer", "to": "completed", - "durationMs": 8, + "durationMs": 1, "message": "미션 완료", - "ts": "2026-05-12T14:53:59.302Z" + "ts": "2026-05-12T15:14:58.520Z" } ], "resilienceMetrics": { diff --git a/docs/records/ConnectAI/chronicle.config.json b/docs/records/ConnectAI/chronicle.config.json index cb28e81..cf824bc 100644 --- a/docs/records/ConnectAI/chronicle.config.json +++ b/docs/records/ConnectAI/chronicle.config.json @@ -6,6 +6,6 @@ "description": "Auto-detected from the local project path in the conversation.", "corePurpose": "Capture project direction, architecture discussion, decisions, and development notes as Markdown.", "detailLevel": "standard", - "createdAt": "2026-05-10T07:42:38.921Z", - "updatedAt": "2026-05-10T07:42:38.928Z" + "createdAt": "2026-05-12T15:13:04.937Z", + "updatedAt": "2026-05-12T15:13:04.938Z" } diff --git a/docs/records/ConnectAI/development/2026-05-12_volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이_implementation-2.md b/docs/records/ConnectAI/development/2026-05-12_volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이_implementation-2.md new file mode 100644 index 0000000..941eeff --- /dev/null +++ b/docs/records/ConnectAI/development/2026-05-12_volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이_implementation-2.md @@ -0,0 +1,22 @@ +# Development Log: /Volumes/Data/project/Antigravity/ConnectAI 분석하고 부족한 부분이나 개선이 필요한 부분을 알려주면 좋겠어. + +## Purpose +Record the implementation or verification outcome from the current conversation. + +## Implementation Summary +## 요약 현재 프로젝트 구조는 기본적인 개발 환경 설정 파일과 아스트라(Astra)의 배포 관련 파일들로 구성되어 있습니다. 전반적으로 기능적인 코드가 눈에 띄지 않아, 이 디렉토리 자체만으로는 시스템 전체의 아키텍처나 로직 흐름을 파악하기 어렵습니다. 개선이 필요하다면, 핵심 비즈니스 로직과 관련된 소스 코드와 API 문서화가 가장 시급합니다. ## 상세 설명 - **현재 구조 분석:** `/Volumes/Data/project/Antigravity/ConnectAI` 디렉토리는 프로젝트의 메타 정보(README.md, LICENSE, PATCHNOTES.md), 의존성 관리 파일(package.json, package-lock.json), 빌드 설정(jest.config.js, tsconfig.json), 그리고 여러 버전의 Astra 관련 바이너리 및 스킨 파일들(`astra-*.vsix`)을 포함하고 있습니다. - **핵심 문제:** 이 디렉토리에는 `src/` 폴더가 존재... + +## Architecture +Captured automatically from the assistant response and active project context. + +## Changed Files +- ARCHITECTURE_ANALYSIS.md + +## Dependency Notes +No new dependency note was captured automatically. + +## Bugs +No bugs recorded. + +## Lessons +- Automatic project records should be generated in the background when the turn contains durable project knowledge. diff --git a/docs/records/ConnectAI/development/2026-05-12_volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이_implementation.md b/docs/records/ConnectAI/development/2026-05-12_volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이_implementation.md new file mode 100644 index 0000000..dde6bcb --- /dev/null +++ b/docs/records/ConnectAI/development/2026-05-12_volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이_implementation.md @@ -0,0 +1,22 @@ +# Development Log: /Volumes/Data/project/Antigravity/ConnectAI 분석하고 부족한 부분이나 개선이 필요한 부분을 알려주면 좋겠어. + +## Purpose +Record the implementation or verification outcome from the current conversation. + +## Implementation Summary +## 요약 + +## Architecture +Captured automatically from the assistant response and active project context. + +## Changed Files +- No explicit changed file list was captured automatically. + +## Dependency Notes +No new dependency note was captured automatically. + +## Bugs +No bugs recorded. + +## Lessons +- Automatic project records should be generated in the background when the turn contains durable project knowledge. diff --git a/docs/records/ConnectAI/timeline.md b/docs/records/ConnectAI/timeline.md index 3ecc53a..665efe4 100644 --- a/docs/records/ConnectAI/timeline.md +++ b/docs/records/ConnectAI/timeline.md @@ -84,3 +84,9 @@ ## 2026-05-10 - Auto decision record created: decisions/ADR-0007-volumes-data-project-antigravity-connectai-이거에-기능-개선을-하고-싶어-.md + +## 2026-05-12 +- Auto development record created: development/2026-05-12_volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이_implementation.md + +## 2026-05-12 +- Auto development record created: development/2026-05-12_volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이_implementation-2.md diff --git a/package.json b/package.json index 51060a0..ac20e22 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.37", + "version": "2.80.38", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/agent.ts b/src/agent.ts index abab8e4..d3265ce 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -45,6 +45,7 @@ import { extractVisibleFinal, shouldFinalOnlyRetry, shouldAutoContinue, + looksCutOff, mergeContinuationParts, buildContinuationUserPrompt, FINAL_ONLY_DIRECTIVE, @@ -485,27 +486,36 @@ export class AgentExecutor { let fullSystemPrompt: string; if (isAgentMode) { - // 1. 기본 시스템 프롬프트에서 에이전트 포맷과 충돌하는 섹션 제거 + // The Agent's prompt IS the primary directive (role / persona / tone / output format), + // so it LEADS the system prompt — models anchor on the first persona they see, not the + // last, especially small ones. The Astra base prompt is reduced to neutral scaffolding + // (action tags, current date, anti-leak rules) and follows; a short reminder at the very + // end keeps the model from drifting back to a generic assistant. const strippedSystemPrompt = this.stripAstraFormattingForAgentMode(systemPrompt); + const agentPromptText = (options.agentSkillContext || '').trim(); + if (estimateTokens(agentPromptText) > Math.floor(config.contextLength * 0.5)) { + logInfo('Agent prompt is unusually large relative to the context window.', { + model: actualModel, agentPromptTokens: estimateTokens(agentPromptText), contextLength: config.contextLength, + }); + } - // 2. Astra 전용 컨텍스트는 에이전트 모드에서 비활성화 - // (astraStanceCtx, thinkingPartnerCtx, v4PolicyCtx → 에이전트 역할과 충돌) - const agentDirective = [ - '\n\n[AGENT MODE — ABSOLUTE OVERRIDE]', - 'You are NOT operating as Astra for this response.', - 'A specialized Agent has been selected by the user.', - 'ALL output format, role, persona, and style instructions from the Agent below', - 'take ABSOLUTE PRECEDENCE over any previous formatting rules (including ## 요약, ## 상세 설명, ## 제안).', - 'You MUST follow the Agent\'s 📄 Output Format exactly. Do NOT fall back to Astra\'s default format.', + const agentBlock = [ + '[AGENT MODE — PRIMARY DIRECTIVE]', + 'A specialized Agent has been selected by the user. The Agent System Prompt below is your', + 'PRIMARY directive: it defines your role, persona, tone, and output format. Follow it exactly.', + 'Everything after the Agent block (action-tag reference, date, brain/project context) is technical', + 'scaffolding — use it only as the Agent\'s task requires. Do NOT impose a generic assistant', + 'format (e.g. ## 요약 / ## 상세 설명 / ## 제안) unless the Agent explicitly asks for one.', '', '--- AGENT SYSTEM PROMPT START ---', - options.agentSkillContext, - '--- AGENT SYSTEM PROMPT END ---' + agentPromptText || '(this agent has no instructions yet — fall back to being a concise, direct assistant)', + '--- AGENT SYSTEM PROMPT END ---', ].join('\n'); + const agentTailReminder = '\n\n[REMINDER] You are operating as the Agent defined above. Keep its role, persona, and output format. Do not fall back to a default assistant style or section format.'; - // 3. 조립: 기본(축소) → 유틸리티 컨텍스트 → 에이전트 프롬프트(최후단) - // [CONTEXT] … [/CONTEXT] 사이만 컨텍스트 초과 시 trim 대상 — agentDirective/negative 는 보호. - fullSystemPrompt = `${strippedSystemPrompt}${internetCtx}${memoryCtx}${designerCtx}${secondBrainTraceCtx}\n\n[CONTEXT]\n${knowledgeContextForPrompt}\n${contextBlock}\n[/CONTEXT]\n${negativeCtx}${agentDirective}`; + // [CONTEXT] … [/CONTEXT] 사이만 컨텍스트 초과 시 trim 대상 — agentBlock(앞)·reminder(뒤)·negative 는 보호. + // memoryCtx(RAG/메모리/lessons)도 [CONTEXT] 안에 넣어 토큰이 빡빡할 때 대화 기록보다 먼저 잘리게 한다. + fullSystemPrompt = `${agentBlock}\n\n${strippedSystemPrompt}${internetCtx}${designerCtx}${secondBrainTraceCtx}\n\n[CONTEXT]\n${memoryCtx}\n${knowledgeContextForPrompt}\n${contextBlock}\n[/CONTEXT]\n${negativeCtx}${agentTailReminder}`; } else { // 기존 Astra 모드 (에이전트 미선택) const localProjectKnowledgeCtx = prompt && localPathContext && this.isProjectKnowledgeCreationRequest(prompt) @@ -530,7 +540,8 @@ export class AgentExecutor { const casualCtx = isCasualConversation ? '\n\n[CASUAL CONVERSATION MODE]\nThe user sent a greeting, acknowledgement, or light conversational message. Reply naturally and briefly to the message itself. Do not use Second Brain, memory, project records, reports, references, or analysis unless the user explicitly asks for them.' : ''; - fullSystemPrompt = `${systemPrompt}${internetCtx}${memoryCtx}${designerCtx}${localProjectKnowledgeCtx}${thinkingPartnerCtx}${astraStanceCtx}${secondBrainTraceCtx}${v4PolicyCtx}${casualCtx}\n\n[CONTEXT]\n${knowledgeContextForPrompt}\n${contextBlock}\n[/CONTEXT]\n${negativeCtx}`; + // memoryCtx(RAG/메모리/lessons)는 [CONTEXT] 안에 — 토큰이 빡빡하면 대화 기록보다 먼저 잘림. + fullSystemPrompt = `${systemPrompt}${internetCtx}${designerCtx}${localProjectKnowledgeCtx}${thinkingPartnerCtx}${astraStanceCtx}${secondBrainTraceCtx}${v4PolicyCtx}${casualCtx}\n\n[CONTEXT]\n${memoryCtx}\n${knowledgeContextForPrompt}\n${contextBlock}\n[/CONTEXT]\n${negativeCtx}`; } // ────────────────────────────────────────────────────────────────── // [Context Limit Manager] context length 는 "답변을 그만큼 길게 써도 된다" @@ -980,8 +991,11 @@ export class AgentExecutor { }); } const outputTokens = estimateTokens(assistantContent); - const notice = shouldShowTruncationNotice(stopKind, outputTokens, maxOutputTokens) - ? truncationNotice(stopKind) + // Show the "incomplete" notice when the engine said output-limit/context-overflow/error, + // OR when (after all auto-continuation rounds) the answer still plainly ends mid-sentence. + const notice = + shouldShowTruncationNotice(stopKind, outputTokens, maxOutputTokens) ? truncationNotice(stopKind) + : looksCutOff(assistantContent) ? truncationNotice('output-limit') : ''; if (notice && assistantContent.trim()) { assistantContent = assistantContent.trimEnd() + notice; diff --git a/src/core/responseRecovery.ts b/src/core/responseRecovery.ts index af2ebd4..e94ff85 100644 --- a/src/core/responseRecovery.ts +++ b/src/core/responseRecovery.ts @@ -125,8 +125,36 @@ export function shouldFinalOnlyRetry(cleaned: CleanedAssistantOutput): boolean { } /** - * Should we silently continue from where the answer was cut off? Only when it actually hit the - * output-token ceiling and we already have a non-trivial visible answer to continue from. + * Does the answer plainly end mid-sentence / mid-structure? Conservative — only flags *unambiguous* + * incompleteness (a complete Korean sentence may legitimately end without a period, so we never flag + * a plain syllable like `다`/`요`; we only flag connective particles, mid-English-words, mid-clause + * commas/colons, unclosed code fences/brackets, and dangling markdown bullets/headings). + */ +export function looksCutOff(text: string): boolean { + const t = (text || '').replace(/\s+$/, ''); + if (t.length < 12) return false; + // unclosed code fence + if ((t.match(/```/g) || []).length % 2 === 1) return true; + // ends with an opening bracket / quote (unclosed pair) + if (/[([{“‘"'`]$/.test(t)) return true; + // dangling markdown bullet / heading / blockquote with no content after the marker + if (/(?:^|\n)\s*(?:[-*+]|#{1,6}|>|\d+\.)\s*$/.test(t)) return true; + // ends mid-English-word or mid-number + if (/[A-Za-z0-9]$/.test(t)) return true; + // ends mid-clause (comma / colon / semicolon / list separator) + if (/[,:;·、,]$/.test(t)) return true; + // ends with a Korean particle / connective ending that NEVER closes a sentence + if (/(?:으로|로서|로써|로|의|에서|에게|한테|에|을|를|과|와|이랑|랑|는|은|이|가|도|만|까지|부터|마다|조차|마저|밖에|뿐|처럼|같이|보다|이나|거나|든지|든가|고|며|면서|면|어서|아서|여서|니까|는데|은데|ㄴ데|지만|던|도록)$/.test(t)) return true; + return false; +} + +/** + * Should we silently continue from where the answer was cut off? The point is to recover regardless + * of *why* it stopped, since local engines / SDKs often report the stop reason wrongly or not at all: + * - the engine said it hit the output cap (`output-limit`), OR + * - it generated close to the cap (a complete answer wouldn't dangle that early), OR + * - the visible answer plainly ends mid-sentence and the engine didn't give a clean "done" reason. + * Never continues from a too-short fragment, and never from a clean ending (terminal punctuation). */ export function shouldAutoContinue( stopKind: GenerationStopKind, @@ -134,10 +162,14 @@ export function shouldAutoContinue( outputTokens: number, maxOutputTokens: number ): boolean { - if (stopKind !== 'output-limit') return false; - if (!visibleAnswer || visibleAnswer.trim().length < 40) return false; - if (!Number.isFinite(maxOutputTokens) || maxOutputTokens <= 0) return true; - return outputTokens >= Math.floor(maxOutputTokens * 0.8); + const v = (visibleAnswer || '').trim(); + if (v.length < 24) return false; + // These won't be fixed by generating more text — don't auto-continue. + if (stopKind === 'user-stopped' || stopKind === 'context-overflow' || stopKind === 'error' || stopKind === 'tool-calls') return false; + if (stopKind === 'output-limit') return true; + if (Number.isFinite(maxOutputTokens) && maxOutputTokens > 0 && outputTokens >= Math.floor(maxOutputTokens * 0.85)) return true; + // 'complete' (eosFound) or 'unknown' but the text is plainly unfinished → continue. + return looksCutOff(v); } /** Appended to the system prompt for a final-only retry — the previous reply was reasoning-only. */ diff --git a/src/lmstudio/streamer.ts b/src/lmstudio/streamer.ts index ed59182..3352292 100644 --- a/src/lmstudio/streamer.ts +++ b/src/lmstudio/streamer.ts @@ -128,7 +128,9 @@ export class LMStudioStreamer implements IChatStreamer { logInfo('LM Studio SDK chat stream finished.', { model: trimmedModel, stopReason, tokensYielded: yielded }); } } catch { /* result unavailable on some SDK versions — non-fatal */ } - yield { token: '', stopReason: stopReason ?? 'eosFound' }; + // Don't claim `eosFound` if we couldn't actually read the stop reason — leave it + // undefined so the caller treats it as 'unknown' (and its mid-sentence heuristics kick in). + yield { token: '', stopReason }; return; } diff --git a/src/sidebarProvider.ts b/src/sidebarProvider.ts index 830e9d5..54b3468 100644 --- a/src/sidebarProvider.ts +++ b/src/sidebarProvider.ts @@ -1789,22 +1789,31 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn this._currentSessionBrainId = selectedBrainId; let agentSkillContext = undefined; - if (agentFile && fs.existsSync(agentFile)) { - agentSkillContext = fs.readFileSync(agentFile, 'utf8'); - - // Merge in any external skill .md files the user has mapped to this - // agent. We concatenate into the same agentSkillContext blob so the - // rest of the pipeline (agent.ts, agent-mode override) treats them - // identically to the agent's own .md — no further changes needed. - try { - const entry = getOrCreateAgentEntry(agentFile); - const bundle = loadExternalSkills(entry.skillFolders); - const block = formatSkillsAsPromptBlock(bundle); - if (block) { - agentSkillContext = `${agentSkillContext.trim()}\n\n${block}`; + if (agentFile && agentFile !== 'none' && fs.existsSync(agentFile)) { + const fileContent = fs.readFileSync(agentFile, 'utf8'); + // Guard: a freshly-created agent still has only the placeholder template + // ("# Agent Persona: …\n\nAdd your instructions here…"). Treating that as a real + // agent prompt just confuses the model — fall back to normal mode and tell the user. + const body = fileContent.replace(/^?#\s*Agent\s*Persona\s*:.*$/im, '').trim(); + const isPlaceholder = !body || /^add your instructions here/i.test(body); + if (isPlaceholder) { + logInfo('Selected agent has no real instructions — running without agent mode.', { agentFile }); + this._view?.webview.postMessage({ type: 'lmStudioError', value: '선택한 에이전트에 내용이 없습니다 — 에이전트 프롬프트를 작성한 뒤 다시 시도하세요. (이번 응답은 에이전트 없이 처리합니다)' }); + } else { + agentSkillContext = fileContent; + // Merge in any external skill .md files the user has mapped to this agent. We concatenate + // into the same agentSkillContext blob so the rest of the pipeline (agent.ts, agent-mode + // override) treats them identically to the agent's own .md — no further changes needed. + try { + const entry = getOrCreateAgentEntry(agentFile); + const bundle = loadExternalSkills(entry.skillFolders); + const block = formatSkillsAsPromptBlock(bundle); + if (block) { + agentSkillContext = `${agentSkillContext.trim()}\n\n${block}`; + } + } catch (e: any) { + logError('External skill load failed.', { error: e?.message || String(e) }); } - } catch (e: any) { - logError('External skill load failed.', { error: e?.message || String(e) }); } } diff --git a/tests/responseRecovery.test.ts b/tests/responseRecovery.test.ts index 0b6ddf8..bbd4f39 100644 --- a/tests/responseRecovery.test.ts +++ b/tests/responseRecovery.test.ts @@ -2,6 +2,7 @@ import { extractVisibleFinal, shouldFinalOnlyRetry, shouldAutoContinue, + looksCutOff, mergeContinuationParts, buildContinuationUserPrompt, } from '../src/core/responseRecovery'; @@ -74,14 +75,46 @@ describe('responseRecovery.extractVisibleFinal — thought quarantine', () => { }); }); +describe('responseRecovery.looksCutOff', () => { + it('flags answers that plainly end mid-sentence / mid-structure', () => { + expect(looksCutOff('당신은 복잡한 아이디어나 목표를 구체적인 실행 계획과 체계적인 문서화로')).toBe(true); // ends with the particle "로" + expect(looksCutOff('우리는 이 문제를 해결하기 위해 다음과 같은 단계를')).toBe(true); // ends with object marker "를" + expect(looksCutOff('the implementation is not yet complete and we need to')).toBe(true); // mid-English + expect(looksCutOff('the items are: foo, bar,')).toBe(true); // trailing comma + expect(looksCutOff('here is the code:\n```python\nprint("hi")')).toBe(true); // unclosed fence + expect(looksCutOff('정리하면 다음 항목들이 중요합니다:\n- 첫 번째 항목\n- 두 번째 항목\n- ')).toBe(true); // dangling bullet + }); + it('does NOT flag complete-looking answers', () => { + expect(looksCutOff('이것은 완전히 끝난 답변이고 마침표도 붙어 있습니다.')).toBe(false); + expect(looksCutOff('이것은 마침표 없이 끝나는 한국어 문장입니다')).toBe(false); // ends with "다" — valid + expect(looksCutOff('네, 그렇게 하면 됩니다')).toBe(false); + expect(looksCutOff('done.')).toBe(false); + expect(looksCutOff('짧음')).toBe(false); // too short to judge + }); +}); + describe('responseRecovery.shouldAutoContinue', () => { - it('continues only when output-limit AND a real visible answer AND near the cap', () => { + it('continues when the engine reports the output cap was hit', () => { expect(shouldAutoContinue('output-limit', 'x'.repeat(200), 3500, 4096)).toBe(true); - expect(shouldAutoContinue('output-limit', 'short', 4000, 4096)).toBe(false); // no real answer - expect(shouldAutoContinue('output-limit', 'x'.repeat(200), 100, 4096)).toBe(false); // didn't actually hit the cap - expect(shouldAutoContinue('complete', 'x'.repeat(200), 4000, 4096)).toBe(false); + expect(shouldAutoContinue('output-limit', 'x'.repeat(200), 50, 4096)).toBe(true); // engine said so → trust it + }); + it('continues when generation reached ~the cap even if the engine said "complete"', () => { + expect(shouldAutoContinue('complete', 'x'.repeat(200), 4000, 4096)).toBe(true); + }); + it('continues when the answer plainly ends mid-sentence (engine reason unclear)', () => { + expect(shouldAutoContinue('unknown', '당신은 복잡한 아이디어나 목표를 구체적인 실행 계획과 체계적인 문서화로', 60, 4096)).toBe(true); + expect(shouldAutoContinue('complete', 'the implementation continues here and we still need to', 100, 4096)).toBe(true); + }); + it('does NOT continue from a tiny fragment or a complete-looking answer', () => { + expect(shouldAutoContinue('output-limit', 'short', 4000, 4096)).toBe(false); // < 24 chars + expect(shouldAutoContinue('complete', '이것은 완전히 끝난 답변이고 마침표도 붙어 있습니다.', 100, 4096)).toBe(false); + expect(shouldAutoContinue('unknown', '이것은 마침표 없이 끝나는 한국어 문장이고 충분히 길다고 본다', 100, 4096)).toBe(false); + }); + it('does NOT continue on stop reasons that more text cannot fix', () => { expect(shouldAutoContinue('context-overflow', 'x'.repeat(200), 4000, 4096)).toBe(false); expect(shouldAutoContinue('error', 'x'.repeat(200), 4000, 4096)).toBe(false); + expect(shouldAutoContinue('user-stopped', 'x'.repeat(200), 4000, 4096)).toBe(false); + expect(shouldAutoContinue('tool-calls', 'x'.repeat(200), 4000, 4096)).toBe(false); }); });