fix(agent): stream Ollama responses to prevent prefill timeout
This commit is contained in:
+34
-24
@@ -152,39 +152,50 @@ async function runOllamaLoop(ctx: AgentRunContext): Promise<AgentResult> {
|
||||
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 {
|
||||
response = await retry(
|
||||
() =>
|
||||
ollama.chat({
|
||||
const stream = await ollama.chat({
|
||||
model,
|
||||
messages,
|
||||
tools,
|
||||
keep_alive: -1,
|
||||
think: false,
|
||||
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);
|
||||
stream: true,
|
||||
});
|
||||
|
||||
const lastError = err instanceof Error ? err.message : String(err);
|
||||
log.error(`Ollama error: ${lastError}`);
|
||||
return { success: false, error: `Ollama request failed: ${lastError}` };
|
||||
let content = "";
|
||||
let streamToolCalls: ToolCall[] | undefined;
|
||||
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) {
|
||||
const total = promptTokens + (evalTokens ?? 0);
|
||||
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);
|
||||
|
||||
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";
|
||||
|
||||
/** 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({
|
||||
path: type.string.describe("absolute path to the file to read"),
|
||||
|
||||
Reference in New Issue
Block a user