Skip to content

fix(agents): Ollama chat HITL — sanitize null content and preserve pending approval #344

@w7-mgfcode

Description

@w7-mgfcode

Problem

Two distinct defects break the agent-chat HITL approval flow on local Ollama (agent_default_model=ollama:*), discovered while dogfooding the Chat page.

Bug 1 — null-content crash (FallbackExceptionGroup)

Ollama's OpenAI-compatible /v1/chat/completions rejects any message whose content is JSON null and which carries no tool_calls with 400 invalid message content type: <nil> (stricter than the real OpenAI API). A weak local model can emit a degenerate empty assistant turn (no text, no tool call); PydanticAI serialises it as content: null and then replays that message on its validation-retry, so every retry 400s and the whole run dies with FallbackExceptionGroup (both primary + fallback). Not streaming-specific — hits agent.run() too (any multi-step/tool turn).

Verified truth table (curl → forecastlab-ollama, qwen3:8b):

  • assistant content=null + tool_calls → 200 ✅
  • assistant content=null no tool_calls400invalid message content type: <nil>
  • assistant content="" no tool_calls → 200 ✅

Bug 2 — pending approval dropped on model misbehavior

A gated tool (create_alias/archive_run/save_scenario) sets deps.pending_action the moment it fires (the HITL gate works), but it does NOT halt the run. A weak model rambles past the gate, calls more tools, and exhausts its retry budget → UnexpectedModelBehavior is raised before agent.run() returns. AgentService.stream_chat / chat's except UnexpectedModelBehavior handler then emits a generic "invalid tool call" error and returns without checking deps.pending_action — so the already-captured approval (with valid args) is silently discarded and the Approve card never appears.

Fix

  1. Bug 1: inject a sanitizing httpx.AsyncClient into OllamaProvider(http_client=…) in build_agent_model (app/features/agents/agents/base.py) that coerces outgoing messages[*].content: null → "".
  2. Bug 2: in the UnexpectedModelBehavior handlers of both stream_chat and chat, surface deps.pending_action (persist + emit approval_required / return pending_approval=True) before falling back to the generic error. The gate fired — the approval is valid regardless of post-gate model misbehavior.

Acceptance

  • Outgoing Ollama chat requests never send content: null.
  • When a gated tool fires but the model then misbehaves, the HITL Approve card is surfaced (not a generic error).
  • Existing "invalid tool call" behavior preserved when NO gated tool fired.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions