47b3b9f93a
- v2.2.299 아키텍처 수렴: coreChat 통일(엔진 휴리스틱 3벌 제거), 기업 모드 검색 오케스트레이터 승격, lib/execUtil(실행 래퍼 6곳·Python 탐지 3벌 단일화), lib/kstSchedule(워처 4개 nowInKst 통합), estimateTokens 통합, 설정 접근 규칙 명문화 - v2.2.300 채팅 화면 정리: LiveReasoningFilter(스트리밍 중 <think>/Harmony 추론 토큰 단위 차단), 확신도·검토요청 footer 기본 숨김(계산·Reflection 은 유지) - v2.2.301 문맥·의도 이해: [답변 전 이해 원칙] 상시 주입, 워크플로우 의도 브리핑, Report QA 루프(규칙 레지스트리+실측치+회귀 게이트, 블로그_v3 개념 이식) - v2.2.302 /benchmark 비즈니스 렌즈(가격·수익·운영)+빌드 프롬프트 모드+QA 연계 - v2.2.303 handoff 모드(측정치 무손실 인수인계 문서)+/claude(Claude Code 터미널 위임) - v2.2.304 이식성: 지식 경로 두뇌-상대 규약(pickWikiDir 상대 해석), 이사 체크리스트 - v2.2.305 Claude 구독 엔진: claude: 프로바이더(CLI 위임, 모델 드롭다운 자동 노출, coreChat 지원 — 워크플로우·QA도 구독 모델 가능) - v2.2.306 Tone Guard: AI 상투어 금지 레지스트리(상담사 화법 실사례 8종+대조 예시) - v2.2.307 /benchmark 레이아웃 골격(sectionRoles 결정론 분석, 롤링 배너 즉답), 파트별 실패 격리, 합성 타임아웃 120→300초 검증: tsc 무오류 + jest 888 통과 + esbuild 정상 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
141 lines
6.0 KiB
TypeScript
141 lines
6.0 KiB
TypeScript
import * as vscode from 'vscode';
|
|
import { LiveReasoningFilter } from '../../lib/contextBuilders/liveReasoningFilter';
|
|
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;
|
|
// 라이브 표시 시 추론 구간(<think>/Harmony thought)은 토큰 단위로 차단 — agent.ts 본 스트림과 동일.
|
|
const liveFilter = new LiveReasoningFilter();
|
|
const post = (token: string) => {
|
|
if (params.postLiveDeltas && token) {
|
|
const visible = liveFilter.push(token);
|
|
if (visible) deps.getWebview()?.postMessage({ type: 'streamChunk', value: visible });
|
|
}
|
|
};
|
|
|
|
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 };
|
|
}
|