chore: bump version to 2.80.27 and update core features

This commit is contained in:
g1nation
2026-05-09 01:16:12 +09:00
parent 5ffb472d22
commit 3220a126fd
41 changed files with 4457 additions and 72 deletions
+136 -44
View File
@@ -59,6 +59,19 @@ export interface AgentExecutorOptions {
start: () => void;
end: () => void;
};
/**
* Optional native LM Studio chat streamer. When provided AND the active engine is LM Studio,
* chat completions are streamed via @lmstudio/sdk's WebSocket transport instead of the
* OpenAI-compatible REST endpoint. Falls back to REST when omitted or when the streamer
* itself fails (e.g. SDK reachability error).
*/
lmStudioStreamer?: import('./lmstudio/streamer').IChatStreamer;
/**
* Optional pending-approval queue. When provided, dry-run transactions are also published
* into a queue that drives the Approval Panel webview + status bar badge. The existing
* inline `requiresApproval` chat message is preserved for backwards compatibility.
*/
approvalQueue?: import('./features/approval/approvalQueue').ApprovalQueue;
}
// --- Agent Roles & Workflows ---
@@ -135,6 +148,15 @@ export class AgentExecutor {
.replace(/<rationale>[\s\S]*?<\/rationale>/gi, '')
.replace(/^\s*\[PROBLEM\][\s\S]*?\[GOAL\][\s\S]*?\[REASONING\][^\n]*(?:\n+|$)/i, '')
.replace(/^\s*\[PROBLEM\][\s\S]*?(?:\n\s*\n|$)/i, '')
.replace(/(?:<think(?:ing)?>|<analysis>)[\s\S]*?(?:<\/think(?:ing)?>|<\/analysis>)/gi, '')
// Harmony / GPT-OSS-style channel markers: keep only the `final`
// channel; drop everything else (thought, analysis, commentary).
// The closing form varies by model: `<channel|>`, `<|channel|>`,
// `<|end|>`, `<|return|>`. Match conservatively.
.replace(/<\|?channel\|?>\s*(?:thought|analysis|commentary|reasoning)\b[\s\S]*?<\|?channel\|?>/gi, '')
.replace(/<\|?channel\|?>\s*(?:thought|analysis|commentary|reasoning)\b[\s\S]*?(?=<\|?channel\|?>\s*final\b)/gi, '')
.replace(/<\|?channel\|?>\s*final\b\s*(?:<\|?message\|?>)?/gi, '')
.replace(/<\|?(?:end|return|start|message)\|?>/gi, '')
.trim();
}
@@ -453,61 +475,91 @@ export class AgentExecutor {
logError('AI request timed out.', { timeoutMs: timeout, model: actualModel, loopDepth });
this.abortController?.abort();
}, timeout);
const request = await this.createStreamingRequest({
baseUrl: ollamaUrl,
modelName: actualModel,
reqMessages: messagesForRequest,
temperature
});
const { response, engine, apiUrl } = request;
if (this.isStaleRun(runId)) return;
const engine = resolveEngine(ollamaUrl);
const useLmStudioSdk = engine === 'lmstudio' && !!this.options.lmStudioStreamer;
let apiUrl = '';
let aiResponseText = '';
const reader = response.body?.getReader();
if (!reader) throw new Error("Response body is not readable.");
let buffer = '';
if (loopDepth === 0) {
this.webview.postMessage({ type: 'streamStart' });
this.options.onStreamLifecycle?.start();
}
let buffer = '';
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (this.isStaleRun(runId)) return;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed === 'data: [DONE]') continue;
try {
const raw = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed;
const json = JSON.parse(raw);
const token = engine === 'lmstudio' ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || '';
if (token) {
aiResponseText += token;
}
} catch (e: any) {
logError('Failed to parse streaming chunk.', { engine, apiUrl, chunk: summarizeText(trimmed, 300), error: e?.message || String(e) });
}
if (useLmStudioSdk) {
apiUrl = `${ollamaUrl} (sdk)`;
logInfo('Streaming chat via LM Studio SDK.', { model: actualModel });
try {
const stream = this.options.lmStudioStreamer!.stream({
modelName: actualModel,
messages: messagesForRequest.map((m) => ({ role: m.role, content: m.content })),
temperature,
signal: this.abortController.signal,
});
for await (const { token } of stream) {
if (this.isStaleRun(runId)) return;
if (token) aiResponseText += token;
}
} catch (err: any) {
if (err?.name === 'AbortError' || this.abortController.signal.aborted) {
logInfo('Generation aborted by user.');
} else {
logError('LM Studio SDK chat failed.', { engine, error: err?.message ?? String(err) });
this.webview?.postMessage({ type: 'error', value: `LM Studio: ${err?.message ?? err}` });
}
}
} catch (err: any) {
if (err.name === 'AbortError') {
logInfo('Generation aborted by user.');
} else {
logError('Stream reading error.', { engine, apiUrl, error: err?.message || String(err) });
this.webview?.postMessage({ type: 'error', value: `Connection lost: ${err.message}` });
} else {
const request = await this.createStreamingRequest({
baseUrl: ollamaUrl,
modelName: actualModel,
reqMessages: messagesForRequest,
temperature
});
const { response, apiUrl: restApiUrl } = request;
apiUrl = restApiUrl;
if (this.isStaleRun(runId)) return;
const reader = response.body?.getReader();
if (!reader) throw new Error("Response body is not readable.");
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (this.isStaleRun(runId)) return;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed === 'data: [DONE]') continue;
try {
const raw = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed;
const json = JSON.parse(raw);
const token = engine === 'lmstudio' ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || '';
if (token) {
aiResponseText += token;
}
} catch (e: any) {
logError('Failed to parse streaming chunk.', { engine, apiUrl, chunk: summarizeText(trimmed, 300), error: e?.message || String(e) });
}
}
}
} catch (err: any) {
if (err.name === 'AbortError') {
logInfo('Generation aborted by user.');
} else {
logError('Stream reading error.', { engine, apiUrl, error: err?.message || String(err) });
this.webview?.postMessage({ type: 'error', value: `Connection lost: ${err.message}` });
}
}
}
// Final buffer processing
if (buffer.trim() && buffer.trim() !== 'data: [DONE]') {
// Final buffer processing (REST SSE only — SDK has no trailing buffer)
if (!useLmStudioSdk && buffer.trim() && buffer.trim() !== 'data: [DONE]') {
try {
const trimmed = buffer.trim();
const raw = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed;
@@ -717,13 +769,35 @@ export class AgentExecutor {
private async callAgent(role: AgentRole, prompt: string, modelName: string, options: any): Promise<string> {
const persona = AGENT_PROMPTS[role];
const { ollamaUrl, timeout } = getConfig();
const { ollamaUrl } = getConfig();
const messages: ChatMessage[] = [
{ role: 'system', content: persona },
{ role: 'user', content: prompt }
];
const engine = resolveEngine(ollamaUrl);
let responseText = '';
if (engine === 'lmstudio' && this.options.lmStudioStreamer) {
try {
const stream = this.options.lmStudioStreamer.stream({
modelName,
messages: messages.map((m) => ({ role: m.role, content: m.content })),
temperature: 0.3,
signal: this.abortController?.signal,
});
for await (const { token } of stream) {
if (token) responseText += token;
}
return responseText;
} catch (err: any) {
if (err?.name === 'AbortError' || this.abortController?.signal.aborted) return responseText;
logError('LM Studio SDK callAgent stream failed.', { role, error: err?.message ?? String(err) });
throw err;
}
}
const request = await this.createStreamingRequest({
baseUrl: ollamaUrl,
modelName: modelName,
@@ -731,7 +805,6 @@ export class AgentExecutor {
temperature: 0.3 // Use lower temperature for planning and research
});
let responseText = '';
const reader = request.response.body?.getReader();
if (!reader) throw new Error("Agent response body is not readable.");
@@ -2304,6 +2377,25 @@ export class AgentExecutor {
if (config.dryRun) {
report.push(`\n⚠️ **Dry Run Mode Active**: 위 변경 사항을 확인하고 [승인] 또는 [롤백]을 선택해주세요.`);
this.webview?.postMessage({ type: 'requiresApproval' });
// Mirror the inline-chat approval into the queue feeding the dedicated panel + status bar.
const queue = this.options.approvalQueue;
if (queue) {
const recorded = this.transactionManager.getRecordedFiles();
queue.enqueue(
{
id: `txn-${Date.now()}`,
kind: 'transaction',
title: 'Pending file changes',
summary: `${recorded.length}개 파일 변경 대기 중`,
files: recorded.map(r => r.path),
createdAt: Date.now(),
},
{
approve: () => this.approveTransaction(),
reject: () => this.rejectTransaction(),
}
);
}
// Do NOT commit yet
} else {
this.transactionManager.commit();