chore: bump version to 2.80.27 and update core features
This commit is contained in:
+136
-44
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user