v2.2.313~319: 보고 품질·조사 신뢰성 수술 + 네이버 쇼핑인사이트/검색어트렌드 연동
- 파트별 완성, 웹 실접속 증빙, 턴 의도 브리핑, 본문 불일치 검증, 하위 질문 체크리스트, 사이트 심층 수집 (PATCHNOTES.md v2.2.313~318 참조) - /naver-shopping, /naver-trend: 네이버 데이터랩 API 연동. compare/discover 자동 판별, API 배치 상한 캡슐화, 카테고리 트리 fuzzy 매칭, 자격증명은 Astra 설정 패널 SecretStorage 로 관리
This commit is contained in:
+225
-9
@@ -24,10 +24,13 @@ import { buildAstraModeArchitectureContext } from './lib/contextBuilders/astraMo
|
||||
import { isScheduleRequest, buildScheduleContext } from './lib/contextBuilders/scheduleContext';
|
||||
import { isSelfAssessRequest, isAboutSelf, buildSelfAssessContext } from './lib/contextBuilders/selfAssessContext';
|
||||
import { ensureFeatureInventory } from './extension/featureInventory';
|
||||
import { buildUrlContext } from './lib/contextBuilders/urlContext';
|
||||
import { buildUrlContext, isUrlContextSuccessBlock, buildWebFetchNotice, extractSuccessMeta, detectWebContentMismatch, formatWebContentMismatchFooter, type TurnWebFetchStats } from './lib/contextBuilders/urlContext';
|
||||
import { extractUrls } from './features/web/webFetch';
|
||||
import { looksLikeCorrection, looksLikeBehaviorComplaint, captureCorrection } from './intelligence/correctionLoop';
|
||||
import { isLocalInvestigationPrompt, detectHollowInvestigation, formatHollowInvestigationFooter } from './intelligence/investigationPipeline';
|
||||
import { isLocalInvestigationPrompt, detectHollowInvestigation, formatHollowInvestigationFooter, detectUnreadAnalysis, formatUnreadAnalysisFooter } from './intelligence/investigationPipeline';
|
||||
import { detectTaskType } from './intelligence/requirementGraph';
|
||||
import { shouldBuildTurnIntentBrief, buildTurnIntentBrief, formatTurnIntentBlock } from './intelligence/turnIntentBrief';
|
||||
import { buildSubQuestionBlock, checkSubQuestionCoverage, formatSubQuestionFooter } from './intelligence/subQuestions';
|
||||
import { shouldUseMultiAgentWorkflow } from './lib/contextBuilders/multiAgentRouting';
|
||||
import { buildThinkingPartnerResponseContract } from './lib/contextBuilders/thinkingPartnerContract';
|
||||
import { buildDroppedHistorySummary } from './lib/contextBuilders/droppedHistorySummary';
|
||||
@@ -43,7 +46,7 @@ import {
|
||||
isAnalysisRequest,
|
||||
} from './lib/contextBuilders/promptDetection';
|
||||
import { stripAstraFormattingForAgentMode, computeModeSignature } from './lib/contextBuilders/systemPromptShaping';
|
||||
import { sanitizeAssistantContent, isRestartedAnswer, parseRationale } from './lib/contextBuilders/outputSanitization';
|
||||
import { sanitizeAssistantContent, isRestartedAnswer, parseRationale, splitLsepReasoning, isCloserOnlyAnswer, looksLikeReportBody } from './lib/contextBuilders/outputSanitization';
|
||||
import { LiveReasoningFilter } from './lib/contextBuilders/liveReasoningFilter';
|
||||
import { buildEngineMessageVariants } from './lib/contextBuilders/engineMessages';
|
||||
import { buildMemoryContext as buildMemoryContextFn } from './lib/contextBuilders/memoryContext';
|
||||
@@ -332,6 +335,12 @@ export class AgentExecutor {
|
||||
* KV 캐시도 깨졌다. actionStats 와 같은 이유로 depth 0 진입부에서만 초기화.
|
||||
*/
|
||||
memoryCtxCache: string | null;
|
||||
/**
|
||||
* [v2.2.315] 이번 turn 의 웹 실접속 결과 — URL 이 있는 요청에서 접속 성패를
|
||||
* 답변에 결정론으로 표시(실패=상단 경고, 성공=하단 출처)하기 위한 추적.
|
||||
* depth 0 진입부에서만 초기화 (memoryContext 의 resetTurnContext 와 무관).
|
||||
*/
|
||||
webFetch: TurnWebFetchStats | null;
|
||||
} = {
|
||||
retrieval: null,
|
||||
lessons: [],
|
||||
@@ -342,6 +351,7 @@ export class AgentExecutor {
|
||||
actionStats: { reads: 0, lists: 0, investigates: 0 },
|
||||
investigationModelOverride: null,
|
||||
memoryCtxCache: null,
|
||||
webFetch: null,
|
||||
};
|
||||
|
||||
/** Per-turn state 일괄 정리. turn 시작/abort/load session 시 호출. */
|
||||
@@ -572,6 +582,7 @@ export class AgentExecutor {
|
||||
this._turnCtx.actionStats = { reads: 0, lists: 0, investigates: 0 };
|
||||
this._turnCtx.investigationModelOverride = null;
|
||||
this._turnCtx.memoryCtxCache = null;
|
||||
this._turnCtx.webFetch = null;
|
||||
}
|
||||
|
||||
// 1. Prepare Context
|
||||
@@ -655,14 +666,62 @@ export class AgentExecutor {
|
||||
// v2: Bridge 추출 → 직접 fetch 폴백 (urlContext 내부) + 최대 2개 URL + config 게이트.
|
||||
if (prompt && loopDepth === 0 && !isCasualConversation && getConfig().webAutoFetchEnabled !== false) {
|
||||
const urls = extractUrls(prompt, 2);
|
||||
// [v2.2.315] 접속 성패 추적 — 답변에 결정론으로 표시(실패=상단 경고,
|
||||
// 성공=하단 출처). "실접속 없이 일반론으로 사이트 분석" 실사례의 재발 방지.
|
||||
if (urls.length > 0) {
|
||||
this._turnCtx.webFetch = { urls: urls.slice(), succeeded: [], failed: [] };
|
||||
}
|
||||
for (const url of urls) {
|
||||
try {
|
||||
contextBlock += `\n\n${await buildUrlContext(url)}`;
|
||||
logInfo('URL 컨텍스트 주입 시도.', { url });
|
||||
const block = await buildUrlContext(url);
|
||||
contextBlock += `\n\n${block}`;
|
||||
if (isUrlContextSuccessBlock(block)) {
|
||||
this._turnCtx.webFetch!.succeeded.push(url);
|
||||
(this._turnCtx.webFetch!.succeededDetail ??= []).push(extractSuccessMeta(url, block));
|
||||
} else {
|
||||
const reason = (block.match(/상태: 접근 실패 — ([^\n]*)/)?.[1] || '본문 추출 실패').slice(0, 120);
|
||||
this._turnCtx.webFetch!.failed.push({ url, reason });
|
||||
}
|
||||
logInfo('URL 컨텍스트 주입 시도.', { url, ok: isUrlContextSuccessBlock(block) });
|
||||
} catch (e: any) {
|
||||
this._turnCtx.webFetch!.failed.push({ url, reason: String(e?.message ?? e).slice(0, 120) });
|
||||
logError('URL 컨텍스트 주입 실패 (계속 진행).', { error: e?.message ?? String(e) });
|
||||
}
|
||||
}
|
||||
// [v2.2.318] 사이트 심층 수집 — 루트 URL 분석/평가 요청이면 홈 1장으로는
|
||||
// "글의 퀄리티" 물음에 답할 데이터가 없다. 내부 글 링크 상위 2개를 추가
|
||||
// 수집 (병렬). 실패는 조용히 홈 데이터만으로 진행.
|
||||
try {
|
||||
const first = urls[0];
|
||||
const isRootUrl = urls.length === 1 && !!first && new URL(first).pathname.replace(/\/+$/, '') === '';
|
||||
if (isRootUrl && this._turnCtx.webFetch?.succeeded.length
|
||||
&& getConfig().webDeepAnalyzeEnabled !== false
|
||||
&& /(분석|평가|리뷰|퀄리티|품질|어떤|어떠)/.test(prompt)) {
|
||||
const { collectInternalArticleLinks } = await import('./lib/contextBuilders/urlContext');
|
||||
const articleUrls = await collectInternalArticleLinks(first, 2);
|
||||
if (articleUrls.length > 0) {
|
||||
const blocks = await Promise.all(articleUrls.map(async (au) => {
|
||||
try { return { au, block: await buildUrlContext(au) }; } catch { return null; }
|
||||
}));
|
||||
for (const b of blocks) {
|
||||
if (!b || !isUrlContextSuccessBlock(b.block)) continue;
|
||||
contextBlock += `\n\n${b.block}`;
|
||||
this._turnCtx.webFetch!.succeeded.push(b.au);
|
||||
(this._turnCtx.webFetch!.succeededDetail ??= []).push(extractSuccessMeta(b.au, b.block));
|
||||
}
|
||||
logInfo('사이트 심층 수집.', { root: first, articles: articleUrls });
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
logError('사이트 심층 수집 실패 (홈 데이터로 진행).', { error: e?.message ?? String(e) });
|
||||
}
|
||||
}
|
||||
|
||||
// [v2.2.318] 하위 질문 체크리스트 — 물음이 2개 이상 명시된 요청은 각 물음에
|
||||
// 별도 답변을 강제 (LLM 없음, 규칙 기반). 커버리지는 답변 후 footer 로 검사.
|
||||
if (prompt && loopDepth === 0 && !isCasualConversation) {
|
||||
const subQBlock = buildSubQuestionBlock(prompt);
|
||||
if (subQBlock) contextBlock += `\n\n${subQBlock}`;
|
||||
}
|
||||
|
||||
// [Correction Loop ①] 이 발화가 직전 답변에 대한 *정정*(사실) 또는 *행동/스타일
|
||||
@@ -766,6 +825,30 @@ export class AgentExecutor {
|
||||
const secondBrainTraceCtx = secondBrainTrace
|
||||
? `\n\n${renderSecondBrainTraceContext(secondBrainTrace)}`
|
||||
: '';
|
||||
// [v2.2.316] 턴 의도 브리핑 — retrieval 과 *병렬* 시작 (검색이 1~8초 걸리는
|
||||
// 동안 브리핑이 함께 돌아 체감 추가 지연 최소화). 소형 모델은 "의도를
|
||||
// 파악하라"는 추상 지시를 실행하지 못한다 — 파악된 의도를 텍스트로 받아야
|
||||
// 따른다. 멀티에이전트 보고서 경로 전용이던 의도 브리핑의 전 턴 확장.
|
||||
let intentBriefPromise: Promise<import('./intelligence/turnIntentBrief').TurnIntentBrief | null> | null = null;
|
||||
if (loopDepth === 0 && config.intentBriefEnabled !== false
|
||||
&& shouldBuildTurnIntentBrief(prompt, isCasualConversation)) {
|
||||
const recentCtx = this.chatHistory
|
||||
.filter(m => !m.internal && (m.role === 'user' || m.role === 'assistant'))
|
||||
.slice(-4)
|
||||
.map(m => `${m.role === 'user' ? '사용자' : '아스트라'}: ${String(m.content || '').replace(/\s+/g, ' ').slice(0, 120)}`)
|
||||
.join('\n');
|
||||
const briefModel = (config.intentBriefModel || '').trim() || actualModel;
|
||||
intentBriefPromise = buildTurnIntentBrief({
|
||||
prompt: prompt!,
|
||||
recentContext: recentCtx,
|
||||
llm: async (system, user, maxTokens) => {
|
||||
const { coreChat } = require('./core/services') as typeof import('./core/services');
|
||||
const r = await coreChat({ system, user, model: briefModel, temperature: 0.1, maxTokens, timeoutMs: 12_000 });
|
||||
return r.content;
|
||||
},
|
||||
}).catch(() => null);
|
||||
}
|
||||
|
||||
const retrievalStartMs = Date.now();
|
||||
// [v2.2.311] continuation depth 는 depth 0 의 memoryCtx 를 재사용 — 재검색
|
||||
// 3~8초 제거 + 빈 쿼리 재검색으로 청크가 갈리는 문제 제거 + 프롬프트 안정화
|
||||
@@ -790,6 +873,22 @@ export class AgentExecutor {
|
||||
});
|
||||
this._turnCtx.memoryCtxCache = memoryCtx;
|
||||
}
|
||||
// [v2.2.316] 병렬 시작한 의도 브리핑 수확 — 보호 구역(dynamicBlocks)에 주입.
|
||||
// 실패/타임아웃은 조용히 브리핑 없이 진행 (종전 동작).
|
||||
if (intentBriefPromise) {
|
||||
try {
|
||||
const _brief = await intentBriefPromise;
|
||||
const _blk = formatTurnIntentBlock(_brief);
|
||||
if (_blk) {
|
||||
this._turnCtx.dynamicBlocks.set('turn-intent-brief', _blk);
|
||||
logInfo('턴 의도 브리핑 주입.', {
|
||||
purpose: _brief!.purpose.slice(0, 60),
|
||||
accessPath: _brief!.accessPath,
|
||||
deliverable: _brief!.deliverable.slice(0, 40),
|
||||
});
|
||||
}
|
||||
} catch { /* 브리핑 없이 진행 */ }
|
||||
}
|
||||
if (loopDepth === 0 && !isCasualConversation && this._turnCtx.retrieval) {
|
||||
recordTelemetry({
|
||||
kind: 'retrieval',
|
||||
@@ -1279,10 +1378,35 @@ export class AgentExecutor {
|
||||
});
|
||||
}
|
||||
|
||||
// [v2.2.314] LSEP 구분자 뒤가 "마무리 인사"뿐인 답변 — 모델이 본문 전체를
|
||||
// 추론 구간에 쓰고 가시 답변은 한 줄만 낸 실사례("이상으로 분석을 마칩니다"만
|
||||
// 남고 본문 증발). sanitize 가 마커 이전을 통째로 버리기 전에 여기서 잡아
|
||||
// thought-only 로 간주 → 아래 (b) final-only retry 가 본문을 재생성한다.
|
||||
// 재생성까지 실패하면 마커 이전 텍스트가 보고 본문 형태일 때만 구제 승격.
|
||||
let lsepTrappedBody = '';
|
||||
{
|
||||
const lsep = splitLsepReasoning(cleaned.visible);
|
||||
if (lsep.hasMarker && isCloserOnlyAnswer(lsep.after) && lsep.before.trim().length > 800) {
|
||||
lsepTrappedBody = extractVisibleFinal(lsep.before).visible;
|
||||
cleaned = {
|
||||
...cleaned,
|
||||
visible: lsep.after.trim(),
|
||||
hiddenReasoning: [cleaned.hiddenReasoning, lsep.before].filter(Boolean).join('\n\n---\n\n'),
|
||||
hadHiddenReasoning: true,
|
||||
wasThoughtOnly: true,
|
||||
};
|
||||
logInfo('LSEP 마무리-인사-만 답변 감지 — 본문이 추론 구간에 갇힘, 재생성 시도.', {
|
||||
model: actualModel, trappedChars: lsepTrappedBody.length, closer: cleaned.visible.slice(0, 60),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// (b) Final-only retry — the reply was reasoning-only, no visible answer.
|
||||
// [v2.2.314] depth 제한 제거: thought-only 라운드는 실행할 액션도 가시 본문도
|
||||
// 없으므로 continuation(depth>0)에서도 재생성이 안전하고 필요하다 (분석 보고의
|
||||
// 최종 라운드가 정확히 이 지점에서 실패했다).
|
||||
if (shouldFinalOnlyRetry(cleaned)
|
||||
&& config.finalOnlyRetryOnThoughtLeak
|
||||
&& loopDepth === 0
|
||||
&& !this.abortController?.signal.aborted) {
|
||||
try {
|
||||
this.webview.postMessage({ type: 'autoContinue', value: '답변을 정리하는 중입니다...' });
|
||||
@@ -1305,6 +1429,23 @@ export class AgentExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
// [v2.2.314] 재생성까지 실패(여전히 마무리 인사뿐)했고, LSEP 마커 앞에 갇힌
|
||||
// 텍스트가 명백한 보고 본문 형태(헤딩/불릿/문단 다수)면 구제 승격 — 사용자가
|
||||
// 화면에서 봤다가 지워진 그 본문을 되살린다. 형태가 애매하면(진짜 추론일 수
|
||||
// 있음) 승격하지 않는다 — 생각 과정 노출(v2.2.310에서 수정한 문제)의 회귀 방지.
|
||||
if (lsepTrappedBody
|
||||
&& isCloserOnlyAnswer(cleaned.visible)
|
||||
&& looksLikeReportBody(lsepTrappedBody)) {
|
||||
logInfo('LSEP 갇힌 본문 구제 승격 — 재생성 실패 후 보고 형태 확인됨.', {
|
||||
trappedChars: lsepTrappedBody.length,
|
||||
});
|
||||
cleaned = {
|
||||
...cleaned,
|
||||
visible: `${lsepTrappedBody.trim()}\n\n${cleaned.visible}`.trim(),
|
||||
wasThoughtOnly: false,
|
||||
};
|
||||
}
|
||||
|
||||
// (c) Auto-continuation → src/agent/handlePrompt/applyAutoContinuation.ts
|
||||
let continuationCount = 0;
|
||||
if (config.autoContinueOnOutputLimit && config.maxAutoContinuations > 0 && loopDepth === 0) {
|
||||
@@ -1363,6 +1504,50 @@ export class AgentExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
// [v2.2.313] 파트별 완성 — 이어쓰기(연장)로도 출력 한계에 닿은 보고서형 답변은
|
||||
// "더 좁은 주제로 나눠 질문하세요"로 사용자에게 떠넘기지 않고, 남은 섹션을
|
||||
// 백엔드가 독립 호출로 나눠 생성해 한 문서로 통합한다 (사용자 제안 반영).
|
||||
// 섹션 호출은 시스템+히스토리 프리픽스를 그대로 재사용 → KV 캐시로 프리필 저렴.
|
||||
// 액션 태그가 남은 답변(아직 실행 단계)은 제외 — 완성은 최종 서술 답변에만.
|
||||
if (getConfig().sectionedCompletionEnabled !== false
|
||||
&& cleaned.visible.trim().length >= 600
|
||||
&& !this.abortController?.signal.aborted
|
||||
&& !/<(read_file|list_files|investigate_files|run_command|run_code|create_file|edit_file|delete_file)\b/i.test(cleaned.visible)) {
|
||||
const _stopKindEarly = classifyStopReason(finishStopReason);
|
||||
if (_stopKindEarly === 'output-limit' || looksCutOff(cleaned.visible)) {
|
||||
const lastUserMsg = String([...this.chatHistory].reverse().find(m => m.role === 'user' && !m.internal)?.content || prompt || '');
|
||||
const detectedTask = detectTaskType(lastUserMsg);
|
||||
if (detectedTask || isAnalysisRequest(lastUserMsg)) {
|
||||
try {
|
||||
this.webview.postMessage({ type: 'autoContinue', value: '남은 섹션을 나눠 작성해 한 문서로 통합하는 중...' });
|
||||
const { completeSectioned } = await import('./agent/handlePrompt/sectionedCompletion');
|
||||
const done = await completeSectioned({
|
||||
draft: cleaned.visible,
|
||||
userPrompt: lastUserMsg,
|
||||
baseMessages: messagesForRequest,
|
||||
requiredElements: detectedTask?.elements.map(e => e.label),
|
||||
callLLM: async (messages, maxTokens) => {
|
||||
const r = await this.callNonStreaming({
|
||||
baseUrl: ollamaUrl, modelName: actualModel, engine, messages,
|
||||
temperature: 0.2, maxTokens, contextLength: ctxLimits.contextLength,
|
||||
signal: this.abortController?.signal,
|
||||
});
|
||||
const rc = extractVisibleFinal(r.text);
|
||||
return rc.visible || r.text;
|
||||
},
|
||||
onProgress: (m) => this.webview?.postMessage({ type: 'autoContinue', value: m }),
|
||||
});
|
||||
if (done.sectionsAdded > 0) {
|
||||
cleaned = { ...cleaned, visible: done.merged };
|
||||
finishStopReason = 'eosFound'; // 통합 완성 — 잘림 안내 억제
|
||||
}
|
||||
} catch (e: any) {
|
||||
logError('파트별 완성 실패 — 초안 유지 (종전 안내로 폴백).', { error: e?.message ?? String(e) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 답변 sanitize / policy enforcement → src/agent/handlePrompt/processFinalAnswer.ts
|
||||
const _finalProc = processFinalAnswer({
|
||||
visibleAnswer: cleaned.visible,
|
||||
@@ -1376,6 +1561,7 @@ export class AgentExecutor {
|
||||
actualModel,
|
||||
engine,
|
||||
inputTokens,
|
||||
recordGuardActive: !!options.designerContext,
|
||||
});
|
||||
const cleanedVisible = _finalProc.cleanedVisible;
|
||||
const assistantContent = _finalProc.assistantContent;
|
||||
@@ -1563,7 +1749,29 @@ export class AgentExecutor {
|
||||
// append via streamChunk because the bubble has multiple action
|
||||
// segments and we don't have a single "final" to replace with.
|
||||
if (loopDepth === 0) {
|
||||
this.webview.postMessage({ type: 'streamReplace', value: finalAssistantContent });
|
||||
// [v2.2.315] 웹 실접속 표시 — URL 요청이면 실패는 상단 경고, 성공은 하단 출처.
|
||||
// [v2.2.317] + 본문 불일치 검증 — 접속 성공인데 실데이터 미반영 일반론이면 경고.
|
||||
const _webNotice = buildWebFetchNotice(this._turnCtx.webFetch);
|
||||
const _mismatch = detectWebContentMismatch(finalAssistantContent, this._turnCtx.webFetch);
|
||||
const _mismatchFooter = _mismatch.mismatch ? formatWebContentMismatchFooter() : '';
|
||||
if (_mismatch.mismatch) logInfo('웹 본문 불일치 감지.', { hits: _mismatch.hits, checked: _mismatch.checkedTokens });
|
||||
// [v2.2.318] 하위 질문 커버리지 — 명시된 물음이 답변에서 빠졌으면 목록 표시.
|
||||
const _subCov = checkSubQuestionCoverage(prompt, finalAssistantContent);
|
||||
const _subFooter = formatSubQuestionFooter(_subCov);
|
||||
if (_subFooter) logInfo('하위 질문 누락 감지.', { total: _subCov.total, missing: _subCov.missing.length });
|
||||
this.webview.postMessage({ type: 'streamReplace', value: `${_webNotice.top}${finalAssistantContent}${_webNotice.bottom}${_mismatchFooter}${_subFooter}` });
|
||||
// [v2.2.313] 파일 미확인 분석 경고 — 로컬 경로 분석 요청인데 이 턴에
|
||||
// 액션을 한 번도 안 쓰고 답한 경우 (hollow/ungrounded 가 못 잡는 사각지대).
|
||||
// [v2.2.315] URL 요청은 제외 — 웹 분석은 파일 액션이 필요 없고, 웹 쪽은
|
||||
// 위의 실접속 표시가 담당한다 (koritips.com 오탐 실사례).
|
||||
try {
|
||||
if (prompt && isAnalysisRequest(prompt)
|
||||
&& !(this._turnCtx.webFetch && this._turnCtx.webFetch.urls.length > 0)
|
||||
&& detectUnreadAnalysis(finalAssistantContent, this._turnCtx.actionStats, !!localPathContext)) {
|
||||
this.webview.postMessage({ type: 'streamChunk', value: formatUnreadAnalysisFooter() });
|
||||
logInfo('Unread Analysis 감지 (depth 0, 액션 0회).', { stats: this._turnCtx.actionStats });
|
||||
}
|
||||
} catch { /* 감지 실패가 답변을 막지 않음 */ }
|
||||
recordTelemetry({
|
||||
kind: 'turn',
|
||||
durationMs: Date.now() - turnStartMs,
|
||||
@@ -1574,7 +1782,10 @@ export class AgentExecutor {
|
||||
stopReason: finishStopReason,
|
||||
brainFiles: this._turnCtx.retrieval?.usedBrainFiles.length ?? 0,
|
||||
memoryLayers: this._turnCtx.retrieval?.usedMemoryLayers ?? [],
|
||||
note: `continuations=${continuationCount} historyDropped=${reqMessages.length - budgetedHistory.length}`,
|
||||
note: `continuations=${continuationCount} historyDropped=${reqMessages.length - budgetedHistory.length}`
|
||||
+ (this._turnCtx.webFetch
|
||||
? ` webFetch=${this._turnCtx.webFetch.succeeded.length}/${this._turnCtx.webFetch.urls.length}`
|
||||
: ''),
|
||||
});
|
||||
// ── Post-answer hooks (v2.2.197) — Devil + SelfCheck + TermValidator 통합 레지스트리. ──
|
||||
// 새 hook 추가 = `src/agent/postAnswerHooks/index.ts` 에 한 객체 push.
|
||||
@@ -1597,7 +1808,12 @@ export class AgentExecutor {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.webview.postMessage({ type: 'streamChunk', value: finalAssistantContent });
|
||||
// [v2.2.315] 웹 실접속 표시 — 액션 턴의 최종 라운드에도 동일 적용.
|
||||
const _webNotice2 = buildWebFetchNotice(this._turnCtx.webFetch);
|
||||
const _mismatch2 = detectWebContentMismatch(finalAssistantContent, this._turnCtx.webFetch);
|
||||
const _lastUser2 = String([...this.chatHistory].reverse().find(m => m.role === 'user' && !m.internal)?.content || '');
|
||||
const _subFooter2 = formatSubQuestionFooter(checkSubQuestionCoverage(_lastUser2, finalAssistantContent));
|
||||
this.webview.postMessage({ type: 'streamChunk', value: `${_webNotice2.top}${finalAssistantContent}${_webNotice2.bottom}${_mismatch2.mismatch ? formatWebContentMismatchFooter() : ''}${_subFooter2}` });
|
||||
// [v2.2.309] Hollow Investigation — 액션 turn 의 최종 답변(continuation)은
|
||||
// post-answer hooks(depth 0 전용)를 타지 않으므로 여기서 직접 검사한다.
|
||||
// "list_files 만 하고 read/investigate 없이 파일 여러 개를 서술" = 헛조사.
|
||||
|
||||
Reference in New Issue
Block a user