zeroclaw-labs/zeroclaw · error · SemanticEmptyTerminalCompletion

provider completed without final text or tool calls

Error message

provider completed without final text or tool calls

What it means

In run_tool_call_loop, a candidate response that is semantically empty terminal (no final text and no tool calls) ends the loop with the typed SemanticEmptyTerminalCompletion after recording rejected tool-loop usage for the served provider/model. The loop deliberately does not continue on empty completions: an empty turn is a terminal failure so SOP/reliable delivery boundaries can classify and account it.

Source

Thrown at crates/zeroclaw-runtime/src/agent/turn/mod.rs:934

        // Reliable providers classify this before retries and fallback. Keep
        // the turn-level guard for direct/unwrapped providers: a transport
        // success with no final text and no tool calls cannot complete a turn.
        // This runs before response-success telemetry and history mutation.
        let chat_result = chat_result.and_then(|response| {
            if response.is_semantically_empty_terminal() {
                if let Some(rejected_stream) = provisional_stream_attempt.take()
                    && let Some(usage) = response.usage.clone()
                {
                    rejected_attempts.push(rejected_stream.with_usage(usage));
                }
                if let Some(usage) = response.usage.as_ref() {
                    crate::agent::cost::record_rejected_tool_loop_cost_usage(
                        served_provider,
                        served_model,
                        usage,
                    );
                }
                return Err(anyhow::Error::new(
                    zeroclaw_api::model_provider::SemanticEmptyTerminalCompletion,
                ));
            }
            Ok(response)
        });

        let (
            response_text,
            parsed_text,
            tool_calls,
            assistant_history_content,
            native_tool_calls,
            parse_issue_detected,
            protocol_suppressed,
            response_streamed_live,
            reported_input_tokens,
            response_usage,
        ) = match chat_result {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Increase max_tokens and ensure the system prompt requires a final answer or tool call, not open-ended reasoning.
  2. Chain a non-reasoning model as fallback in the reliable provider so the loop gets a usable terminal.
  3. Review rejected usage metrics to see whether the model is burning tokens on hidden reasoning before failing.
  4. If the provider intermittently returns empty 200s, check gateway logs/content filters for those request ids.

Example fix

# before: system prompt invites endless reasoning
system = "Think carefully about which tool to use..."

# after: force a terminal action and budget for reasoning
system = "Reason briefly, then ALWAYS answer with text or one tool call."
max_tokens = 8192
Defensive patterns

Strategy: fallback

Validate before calling

fn turn_output_is_usable(resp: &ChatResponse) -> bool {
    !resp.tool_calls.is_empty()
        || resp.text.as_deref().map(|t| !strip_think_tags(t).trim().is_empty()).unwrap_or(false)
}

Try / catch

match turn::run_tool_call_loop(&ctx, input).await {
    Ok(out) => Ok(out),
    Err(e) if e.is::<SemanticEmptyTerminalCompletion>() => {
        // loop terminated on empty turn; rerun once on a plain model before surfacing
        ctx.with_model(plain_model()).run_tool_call_loop(input).await
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: The tool-call loop queries the served model and gets think-only or empty output with zero tool calls: the loop records record_rejected_tool_loop_cost_usage for the attempt and returns Err(SemanticEmptyTerminalCompletion) instead of iterating again.

Common situations: Agentic runs on reasoning models with tight token budgets, models that respond to tool prompts with reasoning only, upstream returning empty content during load spikes, misconfigured prompts that ask the model to 'think step by step' so it never emits an action.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/3df2a58b9a0c1e52. Report an issue: GitHub.