fix(agents,ui): Ollama chat HITL — null-content, pending-approval salvage, and approval report#345
Conversation
Ollama's OpenAI-compatible /v1/chat/completions rejects any message whose content is JSON null and carries no tool_calls (400 'invalid message content type: <nil>'). A weak local model emits that shape for an empty assistant turn and PydanticAI replays it on retry, so every retry 400s and the run dies with FallbackExceptionGroup. Inject a sanitizing httpx transport into the OllamaProvider client that coerces outgoing content:null to content:"".
…344) A gated tool (create_alias/archive_run/save_scenario) records deps.pending_action the moment it fires, but does not halt the run. A weak model can ramble past the gate and exhaust its retry budget, so agent.run raises UnexpectedModelBehavior before returning and the post-run approval-surfacing path never runs. The captured approval is valid, so surface it: chat() and stream_chat()'s UnexpectedModelBehavior handlers now check deps.pending_action first and emit the approval (ChatResponse pending_approval / approval_required event) before falling back to the generic error.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Sorry @w7-mgfcode, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
) The chat Approve/Reject handlers awaited POST /approve but discarded the ApprovalResponse and only cleared the card, so a click produced no visible result — and a failed action execution (e.g. create_alias 'Run not found', returned as status=rejected + result.error) was silent. Capture the response and append a one-line report for every outcome (executed / approved-but-failed / rejected / expired) via a pure, unit-tested formatApprovalReport helper. Closes #346.
Closes #344. Closes #346.
Three fixes for the agent-chat HITL approval flow on local Ollama, found while dogfooding the Chat page. One PR, three focused commits.
Bug 1 — null-content crash (
FallbackExceptionGroup) — #344Ollama's OpenAI-compatible
/v1/chat/completionsrejects any message whosecontentis JSONnulland carries notool_callswith400 invalid message content type: <nil>. A weak local model emits that shape for an empty assistant turn; PydanticAI replays it on retry, so every retry 400s and the run dies withFallbackExceptionGroup. Not streaming-specific.Verified (curl → Ollama, qwen3:8b):
content=null+ tool_calls → 200;content=nullno tool_calls → 400;content=""→ 200.Fix:
build_agent_modelinjects a sanitizinghttpxtransport intoOllamaProvider(http_client=…)that coerces outgoingmessages[*].content: null → ""(recomputes Content-Length; generous read timeout for slow local generation).Bug 2 — pending approval dropped on model misbehavior — #344
A gated tool records
deps.pending_actionwhen it fires but does not halt the run. A weak model rambles past the gate and exhausts its retry budget →UnexpectedModelBehaviorbeforeagent.run()returns. The handlers emitted a generic "invalid tool call" error without checkingdeps.pending_action, discarding a valid approval.Fix: shared
_salvage_pending_action; bothchat()andstream_chat()UnexpectedModelBehaviorhandlers now surfacedeps.pending_action(pending_approval=True/approval_requiredevent) before the generic error.Bug 3 — approve/reject produced no visible result — #346
The Chat
handleApprove/handleRejectawaitedPOST /approvebut discarded theApprovalResponseand only cleared the card. The operator saw nothing — and a failed execution (create_alias→ "Run not found", returnedstatus=rejected+result.error) was silent.Fix: capture the response and append a one-line report for every outcome (executed / approved-but-failed-with-cause / rejected / expired) via a pure, unit-tested
formatApprovalReporthelper.Testing
test_base.py(null-content truth table + transport rewrite + Content-Length);test_service.py(gated-tool-then-misbehavior →chat()pending_approval=True,stream_chat()emitsapproval_required, generic-error path preserved). 69 agents tests pass.approval-report.test.tscovers all four outcomes;tsc --noEmitclean, eslint clean, 254/254 vitest pass.successrun creates the alias; the execution report renders for success and failure.Notes
Reliable HITL on local Ollama needs a tool-capable model: llama3.1:8b triggers the gate reliably; qwen3:8b often returns empty turns under
PromptedOutput. Use one fresh session per approval attempt.