0a97324f1b
R56–R59: agent.ts 2731→1529줄 god-file 분해 (25 modules) · attrParsers + LLM 메서드 8개 (callNonStreaming, streamChatOnce 등) · executeActions 415줄 → 8 handler 그룹 (file/run/list/brain/calendar/sheets/tasks) · handlePrompt 1100줄 → 7 phase 모듈 (system prompt + budget + autoContinue 등) R50–R55: extension.ts 1145→349줄 (telegram/settings/provider commands 분리) Stocks feature 신규: /stocks slash command (v2.2.152~158) · .astra/stocks.json 저장소 + Yahoo Finance 현재가 갱신 · 8 키워드 필터 (ROE/성장성/유동성/수익성/영업효율/기술력/안정성/PBR) · Naver 시가총액 페이지 JSON API (m.stock.naver.com) 발굴 · LLM Top 5 매력도 분석 + Telegram 자동 보고서 · KST 09:00/15:00 watcher 자동 모니터링 대화 연속성 (v2.2.150~157): · [PRIOR TURN CONCLUSION] block 으로 직전 결론 anchor · thin follow-up 분류 → boilerplate 헤더 suppression · slash 명령 결과 chatHistory mirror (capture wrapper) · echo/parrot 금지 system prompt rule 기타: /stocks 슬래시 자동완성 dropdown UI, Naver JSON API 전환 (cheerio 제거) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
148 lines
6.3 KiB
TypeScript
148 lines
6.3 KiB
TypeScript
import { logError } from '../../utils';
|
|
import { getConfig, BrainProfile } from '../../config';
|
|
import { stripMarkdownFormatting, looksCutOff } from '../../core/responseRecovery';
|
|
import {
|
|
sanitizeAssistantContent,
|
|
parseRationale,
|
|
} from '../../lib/contextBuilders/outputSanitization';
|
|
import {
|
|
isSecondBrainInventoryRequest,
|
|
isNoBrainDataRefusal,
|
|
} from '../../lib/contextBuilders/promptDetection';
|
|
import { buildSecondBrainInventoryFallbackAnswer } from '../../lib/contextBuilders/secondBrainInventory';
|
|
import { isProjectKnowledgeCreationRequest } from '../../lib/contextBuilders/localProjectIntent';
|
|
import {
|
|
buildProjectKnowledgeFallbackAnswer,
|
|
writeProjectKnowledgeRecord,
|
|
} from '../../lib/contextBuilders/projectKnowledge';
|
|
import { enforceLocalPathReviewAnswer } from '../../lib/contextBuilders/localProjectPath';
|
|
import { isBlockingProjectKnowledgeAnswer } from '../../lib/contextBuilders/recentProjectKnowledge';
|
|
import {
|
|
enforceProjectClaimPolicyInAnswer,
|
|
SecondBrainTrace,
|
|
} from '../../features/secondBrainTrace';
|
|
import {
|
|
estimateTokens,
|
|
classifyStopReason,
|
|
truncationNotice,
|
|
shouldShowTruncationNotice,
|
|
} from '../../lib/contextManager';
|
|
|
|
export interface ProcessFinalAnswerInput {
|
|
/** Raw `cleaned.visible` from extractVisibleFinal(). */
|
|
visibleAnswer: string;
|
|
prompt: string | null;
|
|
secondBrainTrace: SecondBrainTrace | null;
|
|
localPathContext: string;
|
|
activeBrain: BrainProfile;
|
|
brainFiles: string[];
|
|
finishStopReason: string | undefined;
|
|
maxOutputTokens: number;
|
|
/** From earlier phases — used in logError noise. */
|
|
actualModel: string;
|
|
engine: string;
|
|
inputTokens: number;
|
|
}
|
|
|
|
export interface ProcessFinalAnswerResult {
|
|
/** post-stripMarkdown 1차 — agent.ts 의 `executeActions(cleanedVisible, …)` 호출에 그대로 전달. */
|
|
cleanedVisible: string;
|
|
/** post-enforcers, pre-final-stripMarkdown — used for executeActions and history. */
|
|
assistantContent: string;
|
|
/** post-stripMarkdown-FINAL — emitted to webview. */
|
|
finalAssistantContent: string;
|
|
rationale: ReturnType<typeof parseRationale>;
|
|
outputTokens: number;
|
|
stopKind: ReturnType<typeof classifyStopReason>;
|
|
}
|
|
|
|
export function processFinalAnswer(input: ProcessFinalAnswerInput): ProcessFinalAnswerResult {
|
|
const {
|
|
visibleAnswer,
|
|
prompt,
|
|
secondBrainTrace,
|
|
localPathContext,
|
|
activeBrain,
|
|
brainFiles,
|
|
finishStopReason,
|
|
maxOutputTokens,
|
|
actualModel,
|
|
engine,
|
|
inputTokens,
|
|
} = input;
|
|
|
|
// [Plain Text Output] outputFormat='plain' (기본)이면 모델이 무심코 내보낸
|
|
// 마크다운 마커(`##`, `**`, `> `, `* ` …) 를 후처리로 모두 제거. 라벨 텍스트는 유지.
|
|
// markdown 모드면 legacy 그대로 통과.
|
|
const cleanedVisible = getConfig().outputFormat === 'plain'
|
|
? stripMarkdownFormatting(visibleAnswer)
|
|
: visibleAnswer;
|
|
|
|
// 5. Execute Actions
|
|
const rationale = parseRationale(cleanedVisible);
|
|
let assistantContent = enforceLocalPathReviewAnswer(
|
|
enforceProjectClaimPolicyInAnswer(
|
|
sanitizeAssistantContent(cleanedVisible),
|
|
secondBrainTrace
|
|
),
|
|
localPathContext
|
|
);
|
|
if (prompt && isSecondBrainInventoryRequest(prompt) && brainFiles.length > 0 && isNoBrainDataRefusal(assistantContent)) {
|
|
assistantContent = buildSecondBrainInventoryFallbackAnswer(activeBrain, brainFiles, secondBrainTrace);
|
|
}
|
|
// Note: a previous implementation replaced LLM review answers with a
|
|
// hardcoded Korean template whenever the answer didn't match enough
|
|
// keywords. That made every review feel canned and project-agnostic
|
|
// (the template was Datacollector-flavored). We now let the LLM's
|
|
// answer stand — the system prompt for review-evaluation
|
|
// (buildLocalProjectIntentGuidance / buildAstraStanceContext) is
|
|
// strong enough to keep the response concrete.
|
|
if (prompt && localPathContext && isProjectKnowledgeCreationRequest(prompt)) {
|
|
const record = writeProjectKnowledgeRecord(localPathContext);
|
|
if (isBlockingProjectKnowledgeAnswer(assistantContent)) {
|
|
assistantContent = buildProjectKnowledgeFallbackAnswer(localPathContext, record);
|
|
} else if (record && !assistantContent.includes(record.filePath)) {
|
|
assistantContent = [
|
|
assistantContent,
|
|
'',
|
|
'## 생성된 기록',
|
|
`프로젝트 지식 기록을 생성했습니다: \`${record.filePath}\``
|
|
].join('\n');
|
|
}
|
|
}
|
|
// Surface truncated/abnormal generation so the user knows the answer is incomplete.
|
|
const stopKind = classifyStopReason(finishStopReason);
|
|
if (stopKind === 'output-limit' || stopKind === 'context-overflow' || stopKind === 'error') {
|
|
logError('Generation stopped abnormally.', {
|
|
model: actualModel, engine, stopReason: finishStopReason, stopKind,
|
|
inputTokens, maxOutputTokens, answerChars: assistantContent.length,
|
|
});
|
|
}
|
|
const outputTokens = estimateTokens(assistantContent);
|
|
// 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;
|
|
}
|
|
// [Plain Text Output — FINAL pass] enforcer 들이 `## 경로 확인 결과` 같은 하드코딩 헤더를
|
|
// 다시 prepend 한 후에도 마커가 남지 않도록, webview / chatHistory 에 들어가는 최종 문자열을
|
|
// 한 번 더 sanitize. cleanedVisible 단계의 1차 sanitize 는 model 출력 자체를 정리하고,
|
|
// 이 2차 sanitize 는 enforcer 출력까지 모두 청소한다.
|
|
const finalAssistantContent = getConfig().outputFormat === 'plain'
|
|
? stripMarkdownFormatting(assistantContent)
|
|
: assistantContent;
|
|
|
|
return {
|
|
cleanedVisible,
|
|
assistantContent,
|
|
finalAssistantContent,
|
|
rationale,
|
|
outputTokens,
|
|
stopKind,
|
|
};
|
|
}
|