chore: version up to 2.80.38 and package with refined recovery

This commit is contained in:
g1nation
2026-05-13 00:15:45 +09:00
parent 6c4bc3494f
commit eb36cec050
15 changed files with 202 additions and 62 deletions
@@ -1,5 +1,5 @@
{ {
"result": "Final report with inconsistencies. This should be long enough to pass validation.", "result": "Final report with inconsistencies. This should be long enough to pass validation.",
"createdAt": 1778597639298, "createdAt": 1778598898519,
"modelVersion": "unknown" "modelVersion": "unknown"
} }
@@ -1,5 +1,5 @@
{ {
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", "result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
"createdAt": 1778597639290, "createdAt": 1778598898518,
"modelVersion": "unknown" "modelVersion": "unknown"
} }
@@ -1,5 +1,5 @@
{ {
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", "result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
"createdAt": 1778597639286, "createdAt": 1778598898517,
"modelVersion": "unknown" "modelVersion": "unknown"
} }
@@ -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", "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": 1778597639302, "createdAt": 1778598898520,
"modelVersion": "unknown" "modelVersion": "unknown"
} }
@@ -1,8 +1,8 @@
{ {
"missionId": "stress_conflict_1778597639274", "missionId": "stress_conflict_1778598898506",
"status": "completed", "status": "completed",
"startTime": "2026-05-12T14:53:59.274Z", "startTime": "2026-05-12T15:14:58.506Z",
"totalElapsedMs": 28, "totalElapsedMs": 14,
"results": { "results": {
"planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", "planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
"researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", "researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
@@ -16,30 +16,30 @@
{ {
"from": "idle", "from": "idle",
"to": "planner", "to": "planner",
"durationMs": 11, "durationMs": 10,
"message": "전략 수립 중...", "message": "전략 수립 중...",
"ts": "2026-05-12T14:53:59.285Z" "ts": "2026-05-12T15:14:58.516Z"
}, },
{ {
"from": "planner", "from": "planner",
"to": "researcher", "to": "researcher",
"durationMs": 1, "durationMs": 1,
"message": "핵심 정보 수집 및 분석 중...", "message": "핵심 정보 수집 및 분석 중...",
"ts": "2026-05-12T14:53:59.286Z" "ts": "2026-05-12T15:14:58.517Z"
}, },
{ {
"from": "researcher", "from": "researcher",
"to": "writer", "to": "writer",
"durationMs": 8, "durationMs": 2,
"message": "최종 리포트 작성 및 편집 중...", "message": "최종 리포트 작성 및 편집 중...",
"ts": "2026-05-12T14:53:59.294Z" "ts": "2026-05-12T15:14:58.519Z"
}, },
{ {
"from": "writer", "from": "writer",
"to": "completed", "to": "completed",
"durationMs": 8, "durationMs": 1,
"message": "미션 완료", "message": "미션 완료",
"ts": "2026-05-12T14:53:59.302Z" "ts": "2026-05-12T15:14:58.520Z"
} }
], ],
"resilienceMetrics": { "resilienceMetrics": {
+2 -2
View File
@@ -6,6 +6,6 @@
"description": "Auto-detected from the local project path in the conversation.", "description": "Auto-detected from the local project path in the conversation.",
"corePurpose": "Capture project direction, architecture discussion, decisions, and development notes as Markdown.", "corePurpose": "Capture project direction, architecture discussion, decisions, and development notes as Markdown.",
"detailLevel": "standard", "detailLevel": "standard",
"createdAt": "2026-05-10T07:42:38.921Z", "createdAt": "2026-05-12T15:13:04.937Z",
"updatedAt": "2026-05-10T07:42:38.928Z" "updatedAt": "2026-05-12T15:13:04.938Z"
} }
@@ -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.
@@ -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.
+6
View File
@@ -84,3 +84,9 @@
## 2026-05-10 ## 2026-05-10
- Auto decision record created: decisions/ADR-0007-volumes-data-project-antigravity-connectai-이거에-기능-개선을-하고-싶어-.md - 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
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "astra", "name": "astra",
"displayName": "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.", "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", "publisher": "g1nation",
"license": "MIT", "license": "MIT",
"icon": "assets/icon.png", "icon": "assets/icon.png",
+32 -18
View File
@@ -45,6 +45,7 @@ import {
extractVisibleFinal, extractVisibleFinal,
shouldFinalOnlyRetry, shouldFinalOnlyRetry,
shouldAutoContinue, shouldAutoContinue,
looksCutOff,
mergeContinuationParts, mergeContinuationParts,
buildContinuationUserPrompt, buildContinuationUserPrompt,
FINAL_ONLY_DIRECTIVE, FINAL_ONLY_DIRECTIVE,
@@ -485,27 +486,36 @@ export class AgentExecutor {
let fullSystemPrompt: string; let fullSystemPrompt: string;
if (isAgentMode) { 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 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 전용 컨텍스트는 에이전트 모드에서 비활성화 const agentBlock = [
// (astraStanceCtx, thinkingPartnerCtx, v4PolicyCtx → 에이전트 역할과 충돌) '[AGENT MODE — PRIMARY DIRECTIVE]',
const agentDirective = [ 'A specialized Agent has been selected by the user. The Agent System Prompt below is your',
'\n\n[AGENT MODE — ABSOLUTE OVERRIDE]', 'PRIMARY directive: it defines your role, persona, tone, and output format. Follow it exactly.',
'You are NOT operating as Astra for this response.', 'Everything after the Agent block (action-tag reference, date, brain/project context) is technical',
'A specialized Agent has been selected by the user.', 'scaffolding — use it only as the Agent\'s task requires. Do NOT impose a generic assistant',
'ALL output format, role, persona, and style instructions from the Agent below', 'format (e.g. ## 요약 / ## 상세 설명 / ## 제안) unless the Agent explicitly asks for one.',
'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.',
'', '',
'--- AGENT SYSTEM PROMPT START ---', '--- AGENT SYSTEM PROMPT START ---',
options.agentSkillContext, agentPromptText || '(this agent has no instructions yet — fall back to being a concise, direct assistant)',
'--- AGENT SYSTEM PROMPT END ---' '--- AGENT SYSTEM PROMPT END ---',
].join('\n'); ].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 대상 — agentBlock(앞)·reminder(뒤)·negative 는 보호.
// [CONTEXT] … [/CONTEXT] 사이만 컨텍스트 초과 시 trim 대상 — agentDirective/negative 는 보호. // memoryCtx(RAG/메모리/lessons)도 [CONTEXT] 안에 넣어 토큰이 빡빡할 때 대화 기록보다 먼저 잘리게 한다.
fullSystemPrompt = `${strippedSystemPrompt}${internetCtx}${memoryCtx}${designerCtx}${secondBrainTraceCtx}\n\n[CONTEXT]\n${knowledgeContextForPrompt}\n${contextBlock}\n[/CONTEXT]\n${negativeCtx}${agentDirective}`; fullSystemPrompt = `${agentBlock}\n\n${strippedSystemPrompt}${internetCtx}${designerCtx}${secondBrainTraceCtx}\n\n[CONTEXT]\n${memoryCtx}\n${knowledgeContextForPrompt}\n${contextBlock}\n[/CONTEXT]\n${negativeCtx}${agentTailReminder}`;
} else { } else {
// 기존 Astra 모드 (에이전트 미선택) // 기존 Astra 모드 (에이전트 미선택)
const localProjectKnowledgeCtx = prompt && localPathContext && this.isProjectKnowledgeCreationRequest(prompt) const localProjectKnowledgeCtx = prompt && localPathContext && this.isProjectKnowledgeCreationRequest(prompt)
@@ -530,7 +540,8 @@ export class AgentExecutor {
const casualCtx = isCasualConversation 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.' ? '\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 는 "답변을 그만큼 길게 써도 된다" // [Context Limit Manager] context length 는 "답변을 그만큼 길게 써도 된다"
@@ -980,8 +991,11 @@ export class AgentExecutor {
}); });
} }
const outputTokens = estimateTokens(assistantContent); const outputTokens = estimateTokens(assistantContent);
const notice = shouldShowTruncationNotice(stopKind, outputTokens, maxOutputTokens) // Show the "incomplete" notice when the engine said output-limit/context-overflow/error,
? truncationNotice(stopKind) // 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()) { if (notice && assistantContent.trim()) {
assistantContent = assistantContent.trimEnd() + notice; assistantContent = assistantContent.trimEnd() + notice;
+38 -6
View File
@@ -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 * Does the answer plainly end mid-sentence / mid-structure? Conservative — only flags *unambiguous*
* output-token ceiling and we already have a non-trivial visible answer to continue from. * 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( export function shouldAutoContinue(
stopKind: GenerationStopKind, stopKind: GenerationStopKind,
@@ -134,10 +162,14 @@ export function shouldAutoContinue(
outputTokens: number, outputTokens: number,
maxOutputTokens: number maxOutputTokens: number
): boolean { ): boolean {
if (stopKind !== 'output-limit') return false; const v = (visibleAnswer || '').trim();
if (!visibleAnswer || visibleAnswer.trim().length < 40) return false; if (v.length < 24) return false;
if (!Number.isFinite(maxOutputTokens) || maxOutputTokens <= 0) return true; // These won't be fixed by generating more text — don't auto-continue.
return outputTokens >= Math.floor(maxOutputTokens * 0.8); 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. */ /** Appended to the system prompt for a final-only retry — the previous reply was reasoning-only. */
+3 -1
View File
@@ -128,7 +128,9 @@ export class LMStudioStreamer implements IChatStreamer {
logInfo('LM Studio SDK chat stream finished.', { model: trimmedModel, stopReason, tokensYielded: yielded }); logInfo('LM Studio SDK chat stream finished.', { model: trimmedModel, stopReason, tokensYielded: yielded });
} }
} catch { /* result unavailable on some SDK versions — non-fatal */ } } 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; return;
} }
+24 -15
View File
@@ -1789,22 +1789,31 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
this._currentSessionBrainId = selectedBrainId; this._currentSessionBrainId = selectedBrainId;
let agentSkillContext = undefined; let agentSkillContext = undefined;
if (agentFile && fs.existsSync(agentFile)) { if (agentFile && agentFile !== 'none' && fs.existsSync(agentFile)) {
agentSkillContext = fs.readFileSync(agentFile, 'utf8'); const fileContent = fs.readFileSync(agentFile, 'utf8');
// Guard: a freshly-created agent still has only the placeholder template
// Merge in any external skill .md files the user has mapped to this // ("# Agent Persona: …\n\nAdd your instructions here…"). Treating that as a real
// agent. We concatenate into the same agentSkillContext blob so the // agent prompt just confuses the model — fall back to normal mode and tell the user.
// rest of the pipeline (agent.ts, agent-mode override) treats them const body = fileContent.replace(/^?#\s*Agent\s*Persona\s*:.*$/im, '').trim();
// identically to the agent's own .md — no further changes needed. const isPlaceholder = !body || /^add your instructions here/i.test(body);
try { if (isPlaceholder) {
const entry = getOrCreateAgentEntry(agentFile); logInfo('Selected agent has no real instructions — running without agent mode.', { agentFile });
const bundle = loadExternalSkills(entry.skillFolders); this._view?.webview.postMessage({ type: 'lmStudioError', value: '선택한 에이전트에 내용이 없습니다 — 에이전트 프롬프트를 작성한 뒤 다시 시도하세요. (이번 응답은 에이전트 없이 처리합니다)' });
const block = formatSkillsAsPromptBlock(bundle); } else {
if (block) { agentSkillContext = fileContent;
agentSkillContext = `${agentSkillContext.trim()}\n\n${block}`; // 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) });
} }
} }
+37 -4
View File
@@ -2,6 +2,7 @@ import {
extractVisibleFinal, extractVisibleFinal,
shouldFinalOnlyRetry, shouldFinalOnlyRetry,
shouldAutoContinue, shouldAutoContinue,
looksCutOff,
mergeContinuationParts, mergeContinuationParts,
buildContinuationUserPrompt, buildContinuationUserPrompt,
} from '../src/core/responseRecovery'; } 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', () => { 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', '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), 50, 4096)).toBe(true); // engine said so → trust it
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); 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('context-overflow', 'x'.repeat(200), 4000, 4096)).toBe(false);
expect(shouldAutoContinue('error', '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);
}); });
}); });