fix: revert streaming, add dedup and format enforcement

This commit is contained in:
2026-05-31 18:34:58 -05:00
parent e79affa257
commit e8b2c9952b
3 changed files with 84 additions and 44 deletions
+38 -40
View File
@@ -152,49 +152,47 @@ async function runOllamaLoop(ctx: AgentRunContext): Promise<AgentResult> {
iterations++;
log.info(`» Ollama turn ${iterations}/${MAX_ITERATIONS}...`);
// 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;
// Non-streaming with a heartbeat timer so the activity monitor stays alive
// during long prefill. Streaming was tried but Ollama only emits one tool
// call per chunk — batched tool calls collapse to one-per-turn, turning a
// 7-turn run into 26 turns. The heartbeat fires every 60s to prevent the
// 300s activity timeout from triggering during large-context prefill.
let response: Awaited<ReturnType<typeof ollama.chat>>;
const turnStart = Date.now();
const heartbeat = setInterval(() => {
log.info(`» still waiting for model... (${Math.round((Date.now() - turnStart) / 1000)}s)`);
}, 60_000);
try {
const stream = await ollama.chat({
model,
messages,
tools,
keep_alive: -1,
think: false,
options: { num_ctx: 262144, temperature: 0.1 },
stream: true,
});
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 };
response = await retry(
() => 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) {
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;
clearInterval(heartbeat);
await unloadModel(ollama, model);
const lastError = err instanceof Error ? err.message : String(err);
log.error(`Ollama error: ${lastError}`);
return { success: false, error: `Ollama request failed: ${lastError}` };
}
clearInterval(heartbeat);
const promptTokens = response.prompt_eval_count;
const evalTokens = response.eval_count;
const assistantMessage = response.message;
if (promptTokens !== undefined) {
const total = promptTokens + (evalTokens ?? 0);