feat: v2.2.92 → v2.2.158 — god-file 분해 + Stocks feature + 대화 연속성

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>
This commit is contained in:
g1nation
2026-05-25 09:59:32 +09:00
parent 4153f640c2
commit 0a97324f1b
149 changed files with 14628 additions and 6927 deletions
+136
View File
@@ -0,0 +1,136 @@
import * as vscode from 'vscode';
import { logError, summarizeText } from '../../utils';
import { lmStudioSamplingFromConfig, lmStudioRespondExtrasFromConfig } from '../../lib/contextBuilders/lmStudioSampling';
import type { AgentExecutorOptions, ChatMessage } from '../../agent';
export interface StreamChatOnceDeps {
options: AgentExecutorOptions;
getWebview: () => vscode.Webview | undefined;
isStaleRun: (runId: number) => boolean;
createStreamingRequest: (params: {
baseUrl: string;
modelName: string;
reqMessages: ChatMessage[];
temperature: number;
/** Dynamic output-token cap computed from the remaining context budget. */
maxTokens?: number;
/** Model context window in tokens (used for Ollama's num_ctx). */
contextLength?: number;
}) => Promise<{ response: Response; engine: 'lmstudio' | 'ollama'; apiUrl: string }>;
}
export async function streamChatOnce(deps: StreamChatOnceDeps, params: {
runId: number;
useLmStudioSdk: boolean;
engine: 'lmstudio' | 'ollama';
ollamaUrl: string;
modelName: string;
messages: ChatMessage[];
temperature: number;
maxTokens: number;
contextLength: number;
contextOverflowPolicy: 'stopAtLimit' | 'truncateMiddle' | 'rollingWindow';
signal: AbortSignal;
postLiveDeltas: boolean;
}): Promise<{ text: string; stopReason?: string; aborted: boolean }> {
let accumulated = '';
let finishStopReason: string | undefined;
const post = (token: string) => {
if (params.postLiveDeltas && token) {
deps.getWebview()?.postMessage({ type: 'streamChunk', value: token });
}
};
if (params.useLmStudioSdk) {
try {
const stream = deps.options.lmStudioStreamer!.stream({
modelName: params.modelName,
messages: params.messages.map((m) => ({ role: m.role, content: m.content })),
temperature: params.temperature,
maxTokens: params.maxTokens,
contextOverflowPolicy: params.contextOverflowPolicy,
...lmStudioSamplingFromConfig(),
...lmStudioRespondExtrasFromConfig(),
signal: params.signal,
});
for await (const { token, stopReason } of stream) {
if (deps.isStaleRun(params.runId)) {
return { text: accumulated, stopReason: finishStopReason, aborted: true };
}
if (token) {
accumulated += token;
post(token);
}
if (stopReason) finishStopReason = stopReason;
}
} catch (err: any) {
if (err?.name === 'AbortError' || params.signal.aborted) {
return { text: accumulated, stopReason: finishStopReason, aborted: true };
}
const msg = err?.message ?? String(err);
if (/context\s*length|contextlengthreached|exceed|too\s*long/i.test(msg)) {
finishStopReason = 'contextLengthReached';
}
logError('streamChatOnce SDK path failed.', { engine: params.engine, error: msg });
throw err;
}
return { text: accumulated, stopReason: finishStopReason, aborted: false };
}
const request = await deps.createStreamingRequest({
baseUrl: params.ollamaUrl,
modelName: params.modelName,
reqMessages: params.messages,
temperature: params.temperature,
maxTokens: params.maxTokens,
contextLength: params.contextLength,
});
const reader = request.response.body?.getReader();
if (!reader) throw new Error('Response body is not readable.');
const decoder = new TextDecoder();
let buffer = '';
const consumeJsonLine = (line: string) => {
const trimmed = line.trim();
if (!trimmed || trimmed === 'data: [DONE]') return;
try {
const raw = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed;
const json = JSON.parse(raw);
const token = params.engine === 'lmstudio'
? json.choices?.[0]?.delta?.content || ''
: json.message?.content || json.response || '';
if (token) {
accumulated += token;
post(token);
}
const fr = params.engine === 'lmstudio'
? json.choices?.[0]?.finish_reason
: (json.done_reason ?? (json.done === true ? 'stop' : undefined));
if (fr) finishStopReason = fr;
} catch (e: any) {
logError('streamChatOnce: failed to parse chunk.', { engine: params.engine, chunk: summarizeText(trimmed, 200), error: e?.message ?? String(e) });
}
};
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (deps.isStaleRun(params.runId)) {
return { text: accumulated, stopReason: finishStopReason, aborted: true };
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) consumeJsonLine(line);
}
if (buffer.trim()) consumeJsonLine(buffer);
} catch (err: any) {
if (err?.name === 'AbortError') {
return { text: accumulated, stopReason: finishStopReason, aborted: true };
}
logError('streamChatOnce REST path failed.', { engine: params.engine, error: err?.message ?? String(err) });
throw err;
} finally {
try { reader.releaseLock(); } catch { /* already released on abort */ }
}
return { text: accumulated, stopReason: finishStopReason, aborted: false };
}