Skip to content

Turn finalization: empty assistant row on EmptyProviderResponse, stale error slot, duplicate TurnCompleted #4457

Description

@senamakel

Part of the v0.58.7 → HEAD TinyAgents-migration audit (see parent issue). Three related defects in turn finalization (session/turn/core.rs + tinyagents/mod.rs).

A. EmptyProviderResponse leaves a blank assistant message in history

core.rs:940 extends self.history with outcome.conversation before the empty-response check at core.rs:986-994. The #4093 branch pops the dangling blank assistant row — but only in the tool_calls > 0 branch (core.rs:1032-1043). The tool_calls == 0 branch returns Err(EmptyProviderResponse) with the empty assistant row left in history. Legacy returned the error before history.push (v0.58.7 engine/core.rs:702-730).
Scenario: provider hiccups with an empty completion → user retries → next request carries an empty-content assistant message → strict providers (Anthropic: "text content blocks must be non-empty") 400 — wedging the thread, not just one turn.
Fix: on the EmptyProviderResponse path, pop the trailing empty assistant message before returning (same treatment as the #4093 branch), or move the history extend after the validity checks.

B. Stale provider error masks the real run failure

mod.rs:643 reads error_slot before the LimitExceeded → MaxIterationsExceeded mapping (mod.rs:651). ProviderModel writes the slot on every provider error (model.rs:483,597) including ones recovered by the fallback chain, and never clears it on later success. A turn that failed over successfully and then failed for an unrelated reason returns the stale provider error — wrong classification, wrong Sentry suppression, wrong user message.
Fix: clear error_slot on each successful model call; only consult it when the run error is actually a model error.

C. Duplicate TurnCompleted, emitted before the wrap-up streams

mod.rs:731-735 emits TurnCompleted per observed parent turn and the chat session emits it again (core.rs:1132-1135). The web bridge (web/progress_bridge.rs:1020-1056) records two ledger events + two Completed upserts, and turn_active = false lands at the first emit — on the cap/#4093 paths that's before summarize_turn_wrapup streams the checkpoint text, so the UI is told the turn ended while a model call is still streaming.
Fix: single emission point — suppress the seam-level emit on the chat path (or the session-level one) and move it after the wrap-up completes.

Acceptance criteria

  • Empty-completion error leaves history free of empty assistant rows (assert retry request validates against Anthropic-strict rules).
  • Fallback-recovered provider errors never surface as the turn's failure reason.
  • Exactly one TurnCompleted per turn, after all streamed output.

Metadata

Metadata

Assignees

No one assigned

    Labels

    agentBuilt-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/.bugpriority: mediumEdge case or race conditionsubtaskSubtask of a larger tracked effort.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions