diff --git a/CHANGELOG.md b/CHANGELOG.md index 619c2591922..8b4db1a2289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ Docs: https://docs.openclaw.ai - Providers/Ollama: add provider-scoped model request timeouts, thread them through guarded fetch connect/header/body/abort handling, and document `params.keep_alive` for cold local models so first-turn Ollama loads no longer require global agent timeout changes. Fixes #64541 and #68796; supersedes #65143 and #66511. Thanks @LittleJakub, @Juankcba, @uninhibite-scholar, and @yfge. - Providers/Ollama: preserve explicit configured model input modalities when merging discovered provider metadata so custom vision models keep image support instead of silently dropping attachments. Fixes #39690; carries forward #39785. Thanks @Skrblik and @Mriris. - Providers/Ollama: estimate native Ollama transcript usage when `/api/chat` omits prompt/eval counters while preserving exact zero counters, keeping local model runs visible in usage surfaces. Carries forward #39112. Thanks @TylonHH. +- Agents/Ollama: retry native Ollama turns that finish without user-visible text, including unsigned thinking-only responses, so constrained reasoning turns can continue instead of surfacing an empty reply. Carries forward #66552 and #61223. Thanks @yfge and @L3G. - Providers/PDF/Ollama: add bounded network timeouts for Ollama model pulls and native Anthropic/Gemini PDF analysis requests so unresponsive provider endpoints no longer hang sessions indefinitely. Fixes #54142; supersedes #54144 and #54145. Thanks @jinduwang1001-max and @arkyu2077. - LLM Task/Ollama: accept model overrides that already include the selected provider prefix, avoiding doubled ids such as `ollama/ollama/llama3.2:latest`, and live-verify local Ollama JSON tasks return parsed output. Fixes #50052. Thanks @ralphy-maplebots and @Hollychou924. - Memory/doctor: treat Ollama memory embeddings as key-optional so `openclaw doctor` no longer warns about a missing API key when the gateway reports embeddings are ready. Fixes #46584. Thanks @fengly78. diff --git a/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts b/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts index bd0b27ab1fe..0a9c0383368 100644 --- a/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts +++ b/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts @@ -976,6 +976,103 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { expect(retryInstruction).toBe(REASONING_ONLY_RETRY_INSTRUCTION); }); + it("does not apply planning-only or ack fast paths to Ollama runs", () => { + const retryInstruction = resolvePlanningOnlyRetryInstruction({ + provider: "ollama", + modelId: "gemma4:31b", + prompt: "Please inspect the code, make the change, and run the checks.", + aborted: false, + timedOut: false, + attempt: makeAttemptResult({ + assistantTexts: ["I'll inspect the code, make the change, and run the checks."], + }), + }); + const ackInstruction = resolveAckExecutionFastPathInstruction({ + provider: "ollama", + modelId: "gemma4:31b", + prompt: "go ahead", + }); + + expect(retryInstruction).toBeNull(); + expect(ackInstruction).toBeNull(); + }); + + it("retries signed reasoning-only Ollama turns with a visible-answer continuation instruction", () => { + const retryInstruction = resolveReasoningOnlyRetryInstruction({ + provider: "ollama", + modelId: "gemma4:31b", + aborted: false, + timedOut: false, + attempt: makeAttemptResult({ + assistantTexts: [], + lastAssistant: { + role: "assistant", + stopReason: "end_turn", + provider: "ollama", + model: "gemma4:31b", + content: [ + { + type: "thinking", + thinking: "internal reasoning", + thinkingSignature: JSON.stringify({ id: "ollama_rs_helper", type: "reasoning" }), + }, + ], + } as unknown as EmbeddedRunAttemptResult["lastAssistant"], + }), + }); + + expect(retryInstruction).toBe(REASONING_ONLY_RETRY_INSTRUCTION); + }); + + it("retries unsigned-thinking Ollama turns via the empty-response path", () => { + const retryInstruction = resolveEmptyResponseRetryInstruction({ + provider: "ollama", + modelId: "gemma4:31b", + payloadCount: 0, + aborted: false, + timedOut: false, + attempt: makeAttemptResult({ + assistantTexts: [], + lastAssistant: { + role: "assistant", + stopReason: "end_turn", + provider: "ollama", + model: "gemma4:31b", + content: [ + { + type: "thinking", + thinking: "internal reasoning", + }, + ], + } as unknown as EmbeddedRunAttemptResult["lastAssistant"], + }), + }); + + expect(retryInstruction).toBe(EMPTY_RESPONSE_RETRY_INSTRUCTION); + }); + + it("retries generic empty Ollama turns without visible text", () => { + const retryInstruction = resolveEmptyResponseRetryInstruction({ + provider: "ollama", + modelId: "gemma4:31b", + payloadCount: 0, + aborted: false, + timedOut: false, + attempt: makeAttemptResult({ + assistantTexts: [], + lastAssistant: { + role: "assistant", + stopReason: "end_turn", + provider: "ollama", + model: "gemma4:31b", + content: [{ type: "text", text: "" }], + } as unknown as EmbeddedRunAttemptResult["lastAssistant"], + }), + }); + + expect(retryInstruction).toBe(EMPTY_RESPONSE_RETRY_INSTRUCTION); + }); + it("treats exact NO_REPLY as a deliberate silent assistant reply", () => { const incompleteTurnText = resolveIncompleteTurnPayloadText({ payloadCount: 0, diff --git a/src/agents/pi-embedded-runner/run/incomplete-turn.ts b/src/agents/pi-embedded-runner/run/incomplete-turn.ts index 332a25dd7ee..e7268b5787c 100644 --- a/src/agents/pi-embedded-runner/run/incomplete-turn.ts +++ b/src/agents/pi-embedded-runner/run/incomplete-turn.ts @@ -109,6 +109,7 @@ const GEMINI_INCOMPLETE_TURN_PROVIDER_IDS = new Set([ "google-gemini-cli", ]); const GEMINI_INCOMPLETE_TURN_MODEL_ID_PATTERN = /^gemini(?:[.-]|$)/; +const OLLAMA_INCOMPLETE_TURN_PROVIDER_ID_PATTERN = /^ollama(?:-|$)/; const DEFAULT_PLANNING_ONLY_RETRY_LIMIT = 1; const STRICT_AGENTIC_PLANNING_ONLY_RETRY_LIMIT = 2; // Allow one immediate continuation plus one follow-up continuation before @@ -498,7 +499,7 @@ export function resolveReasoningOnlyRetryInstruction(params: { } if ( - !shouldApplyPlanningOnlyRetryGuard({ + !shouldApplyNonVisibleTurnRetryGuard({ provider: params.provider, modelId: params.modelId, executionContract: params.executionContract, @@ -544,7 +545,7 @@ export function resolveEmptyResponseRetryInstruction(params: { } if ( - shouldApplyPlanningOnlyRetryGuard({ + shouldApplyNonVisibleTurnRetryGuard({ provider: params.provider, modelId: params.modelId, executionContract: params.executionContract, @@ -573,6 +574,19 @@ function shouldApplyPlanningOnlyRetryGuard(params: { }); } +function shouldApplyNonVisibleTurnRetryGuard(params: { + provider?: string; + modelId?: string; + executionContract?: string; +}): boolean { + if (shouldApplyPlanningOnlyRetryGuard(params)) { + return true; + } + return OLLAMA_INCOMPLETE_TURN_PROVIDER_ID_PATTERN.test( + normalizeLowercaseStringOrEmpty(params.provider ?? ""), + ); +} + function isIncompleteTurnRecoverySupportedProviderModel(params: { provider?: string; modelId?: string;