fix(agent): stream Ollama responses to prevent prefill timeout
This commit is contained in:
+40
-30
@@ -152,39 +152,50 @@ async function runOllamaLoop(ctx: AgentRunContext): Promise<AgentResult> {
|
|||||||
iterations++;
|
iterations++;
|
||||||
log.info(`» Ollama turn ${iterations}/${MAX_ITERATIONS}...`);
|
log.info(`» Ollama turn ${iterations}/${MAX_ITERATIONS}...`);
|
||||||
|
|
||||||
let response: Awaited<ReturnType<typeof ollama.chat>>;
|
// Stream the response so the activity monitor sees tokens arriving during
|
||||||
|
// long prefill. Without streaming, a 50k+ token context can take 200-300s
|
||||||
|
// of silent prefill before the first token, tripping the activity timeout.
|
||||||
|
let assistantMessage: Message;
|
||||||
|
let promptTokens: number | undefined;
|
||||||
|
let evalTokens: number | undefined;
|
||||||
try {
|
try {
|
||||||
response = await retry(
|
const stream = await ollama.chat({
|
||||||
() =>
|
model,
|
||||||
ollama.chat({
|
messages,
|
||||||
model,
|
tools,
|
||||||
messages,
|
keep_alive: -1,
|
||||||
tools,
|
think: false,
|
||||||
keep_alive: -1,
|
options: { num_ctx: 262144, temperature: 0.1 },
|
||||||
think: false,
|
stream: true,
|
||||||
options: { num_ctx: 262144, temperature: 0.1 },
|
});
|
||||||
}),
|
|
||||||
{
|
|
||||||
delaysMs: [3_000, 8_000],
|
|
||||||
shouldRetry: (err) => {
|
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
|
||||||
return /unexpected EOF|XML syntax error|ECONNRESET|ETIMEDOUT|fetch failed/i.test(
|
|
||||||
msg,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
label: `Ollama turn ${iterations}`,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
await unloadModel(ollama, model);
|
|
||||||
|
|
||||||
const lastError = err instanceof Error ? err.message : String(err);
|
let content = "";
|
||||||
log.error(`Ollama error: ${lastError}`);
|
let streamToolCalls: ToolCall[] | undefined;
|
||||||
return { success: false, error: `Ollama request failed: ${lastError}` };
|
let chunkCount = 0;
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
content += chunk.message.content ?? "";
|
||||||
|
if (chunk.message.tool_calls?.length) streamToolCalls = chunk.message.tool_calls;
|
||||||
|
if (chunk.prompt_eval_count !== undefined) promptTokens = chunk.prompt_eval_count;
|
||||||
|
if (chunk.eval_count !== undefined) evalTokens = chunk.eval_count;
|
||||||
|
chunkCount++;
|
||||||
|
// Log a heartbeat every 50 chunks so the activity monitor stays alive
|
||||||
|
if (chunkCount % 50 === 0) log.debug(` streaming… (${chunkCount} chunks)`);
|
||||||
|
}
|
||||||
|
assistantMessage = { role: "assistant", content, tool_calls: streamToolCalls };
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
const isRetryable = /unexpected EOF|XML syntax error|ECONNRESET|ETIMEDOUT|fetch failed/i.test(msg);
|
||||||
|
if (!isRetryable) {
|
||||||
|
await unloadModel(ollama, model);
|
||||||
|
log.error(`Ollama error: ${msg}`);
|
||||||
|
return { success: false, error: `Ollama request failed: ${msg}` };
|
||||||
|
}
|
||||||
|
// Retryable: wait and loop — the outer while will retry on the next iteration
|
||||||
|
log.warning(`» stream error (retryable): ${msg} — retrying in 5s`);
|
||||||
|
await new Promise((r) => setTimeout(r, 5_000));
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const promptTokens = response.prompt_eval_count;
|
|
||||||
const evalTokens = response.eval_count;
|
|
||||||
if (promptTokens !== undefined) {
|
if (promptTokens !== undefined) {
|
||||||
const total = promptTokens + (evalTokens ?? 0);
|
const total = promptTokens + (evalTokens ?? 0);
|
||||||
const pct = Math.round((total / 262144) * 100);
|
const pct = Math.round((total / 262144) * 100);
|
||||||
@@ -196,7 +207,6 @@ async function runOllamaLoop(ctx: AgentRunContext): Promise<AgentResult> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const assistantMessage = response.message;
|
|
||||||
messages.push(assistantMessage);
|
messages.push(assistantMessage);
|
||||||
|
|
||||||
const toolCalls: ToolCall[] | undefined = assistantMessage.tool_calls;
|
const toolCalls: ToolCall[] | undefined = assistantMessage.tool_calls;
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ import type { ToolContext } from "./server.ts";
|
|||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
/** Hard cap on returned content to avoid flooding the model's context window. */
|
/** Hard cap on returned content to avoid flooding the model's context window. */
|
||||||
const MAX_CHARS = 20000;
|
const MAX_CHARS = 12000;
|
||||||
|
|
||||||
export const ReadFileParams = type({
|
export const ReadFileParams = type({
|
||||||
path: type.string.describe("absolute path to the file to read"),
|
path: type.string.describe("absolute path to the file to read"),
|
||||||
|
|||||||
Reference in New Issue
Block a user