This commit is contained in:
Brent Larsen
2026-09-04 22:46:16 -05:00
parent 422bc31a8d
commit 8692217d25
7 changed files with 688 additions and 2 deletions
+139
View File
@@ -0,0 +1,139 @@
const Conversation = require("./conversation");
const lmStudio = require("./lmstudio");
const MAX_TRANSPORT_ATTEMPTS = 10;
const MAX_CONTINUATIONS = 10;
const CONTINUE_PROMPT = [
"Your previous response ended before you produced the requested final answer.",
"Continue from where you left off. Do not restart the task or discard the work you already did.",
"Keep investigating as deeply as necessary. When you have actually finished, provide the final deliverable required by the original prompt."
].join("\n");
module.exports = utils => {
const settings = utils.getSettings("llm");
const client = String(settings.client || "lm-studio").toLowerCase();
if (client !== "lm-studio" && client !== "lmstudio") throw new Error(`Unsupported LLM client: ${settings.client}`);
const llm = lmStudio;
let warmupPromise = null;
function warmUp() {
if (!warmupPromise) warmupPromise = llm.warmUp(settings);
return warmupPromise;
}
function prepare(name, systemPrompt, segments = []) {
const conversation = Conversation.start(systemPrompt);
for (const segment of segments) conversation.add(segment);
return { name: name || "anonymous", conversation };
}
function liveOutput() {
let channel = null;
let wrote = false;
function switchTo(next) {
if (channel === next) return;
if (wrote) process.stdout.write("\n");
process.stdout.write(` --- ${next} ---\n`);
channel = next;
wrote = true;
}
return {
reasoning(text) { switchTo("reasoning"); process.stdout.write(text); },
content(text) { switchTo("response"); process.stdout.write(text); },
end() { if (wrote) process.stdout.write("\n"); }
};
}
function addFallbackContinuation(conversation, reply) {
if (reply.reasoning?.trim()) {
conversation.add({
role: "assistant",
content: [
"[UNFINISHED PRIVATE WORK FROM MY PREVIOUS RESPONSE]",
reply.reasoning.trim(),
"[END UNFINISHED PRIVATE WORK]"
].join("\n")
});
}
conversation.add({ role: "user", content: CONTINUE_PROMPT });
}
async function askConversation(agent, conversation, options = {}) {
await warmUp();
const callerOwnsOutput = typeof options.onReasoning === "function" || typeof options.onContent === "function";
const reasoning = [];
let previousResponseId = null;
let useStatefulContinuation = false;
for (let continuation = 0; continuation <= MAX_CONTINUATIONS; continuation++) {
for (let attempt = 1; attempt <= MAX_TRANSPORT_ATTEMPTS; attempt++) {
const output = !callerOwnsOutput && options.live !== false ? liveOutput() : null;
const sendOptions = {
...options,
onReasoning: options.onReasoning || output?.reasoning,
onContent: options.onContent || output?.content
};
delete sendOptions.live;
if (useStatefulContinuation && previousResponseId) {
sendOptions.previousResponseId = previousResponseId;
sendOptions.continuationInput = CONTINUE_PROMPT;
}
try {
const reply = await llm.send(conversation, settings, sendOptions);
output?.end();
if (reply.reasoning?.trim()) reasoning.push(reply.reasoning.trim());
if (reply.content?.trim()) {
delete reply.responseId;
if (reasoning.length > 0) reply.reasoning = reasoning.join("\n\n--- continuation ---\n\n");
return reply;
}
if (continuation >= MAX_CONTINUATIONS) {
throw Object.assign(new Error(`LLM returned no final response content for ${agent.name} after ${MAX_CONTINUATIONS} continuations.`), { retryable: false });
}
console.error();
console.error(`LLM returned unfinished reasoning with no final response for ${agent.name}; continuing (${continuation + 1}/${MAX_CONTINUATIONS}).`);
if (reply.responseId) {
previousResponseId = reply.responseId;
useStatefulContinuation = true;
} else {
previousResponseId = null;
useStatefulContinuation = false;
addFallbackContinuation(conversation, reply);
}
break;
} catch (error) {
output?.end();
const retryable = error.retryable !== false;
console.error();
console.error(`LLM request failed for ${agent.name} (attempt ${attempt}/${MAX_TRANSPORT_ATTEMPTS}): ${error.message}`);
if (error.cause) console.error(` Cause: ${error.cause.code || error.cause.message || String(error.cause)}`);
if (error.partial) console.error(" Stream ended after partial output; retry will restart this response.");
if (!retryable || attempt === MAX_TRANSPORT_ATTEMPTS) throw error;
const delayMs = Math.min(attempt, 5) * 1000;
console.error(` Retrying in ${delayMs / 1000} second${delayMs === 1000 ? "" : "s"}...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
}
throw new Error(`LLM request for ${agent.name} failed unexpectedly.`);
}
async function ask(agent, input, options = {}) {
const conversation = Conversation.from(agent.conversation);
conversation.add({ role: "user", content: input });
return askConversation(agent, conversation, options);
}
async function followUp(agent, originalInput, previousReply, input, options = {}) {
const conversation = Conversation.from(agent.conversation);
conversation.add({ role: "user", content: originalInput });
conversation.add(previousReply);
conversation.add({ role: "user", content: input });
return askConversation(agent, conversation, options);
}
function model() { return settings.model; }
return { warmUp, prepare, ask, followUp, model };
};