{"record":{"id":"3df2a58b9a0c1e52","repo":"zeroclaw-labs/zeroclaw","slug":"provider-completed-without-final-text-or-tool-call-3df2a5","errorCode":null,"errorMessage":"provider completed without final text or tool calls","messagePattern":"provider completed without final text or tool calls","errorType":"exception","errorClass":"SemanticEmptyTerminalCompletion","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/agent/turn/mod.rs","lineNumber":934,"sourceCode":"        // Reliable providers classify this before retries and fallback. Keep\n        // the turn-level guard for direct/unwrapped providers: a transport\n        // success with no final text and no tool calls cannot complete a turn.\n        // This runs before response-success telemetry and history mutation.\n        let chat_result = chat_result.and_then(|response| {\n            if response.is_semantically_empty_terminal() {\n                if let Some(rejected_stream) = provisional_stream_attempt.take()\n                    && let Some(usage) = response.usage.clone()\n                {\n                    rejected_attempts.push(rejected_stream.with_usage(usage));\n                }\n                if let Some(usage) = response.usage.as_ref() {\n                    crate::agent::cost::record_rejected_tool_loop_cost_usage(\n                        served_provider,\n                        served_model,\n                        usage,\n                    );\n                }\n                return Err(anyhow::Error::new(\n                    zeroclaw_api::model_provider::SemanticEmptyTerminalCompletion,\n                ));\n            }\n            Ok(response)\n        });\n\n        let (\n            response_text,\n            parsed_text,\n            tool_calls,\n            assistant_history_content,\n            native_tool_calls,\n            parse_issue_detected,\n            protocol_suppressed,\n            response_streamed_live,\n            reported_input_tokens,\n            response_usage,\n        ) = match chat_result {","sourceCodeStart":916,"sourceCodeEnd":952,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/agent/turn/mod.rs#L916-L952","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Increase max_tokens and ensure the system prompt requires a final answer or tool call, not open-ended reasoning.","Chain a non-reasoning model as fallback in the reliable provider so the loop gets a usable terminal.","Review rejected usage metrics to see whether the model is burning tokens on hidden reasoning before failing.","If the provider intermittently returns empty 200s, check gateway logs/content filters for those request ids."],"exampleFix":"# before: system prompt invites endless reasoning\nsystem = \"Think carefully about which tool to use...\"\n\n# after: force a terminal action and budget for reasoning\nsystem = \"Reason briefly, then ALWAYS answer with text or one tool call.\"\nmax_tokens = 8192","handlingStrategy":"fallback","validationCode":"fn turn_output_is_usable(resp: &ChatResponse) -> bool {\n    !resp.tool_calls.is_empty()\n        || resp.text.as_deref().map(|t| !strip_think_tags(t).trim().is_empty()).unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":"match turn::run_tool_call_loop(&ctx, input).await {\n    Ok(out) => Ok(out),\n    Err(e) if e.is::<SemanticEmptyTerminalCompletion>() => {\n        // loop terminated on empty turn; rerun once on a plain model before surfacing\n        ctx.with_model(plain_model()).run_tool_call_loop(input).await\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Prompt tool-loop models to always finish with an answer or exactly one tool call.","Reserve reasoning models for chains that end in a plain model; empty turns then self-heal.","Alert when a session ends on this error more than once; repeated empties point at budgets or a degraded upstream."],"tags":["llm","agent-loop","tool-calling","empty-response"],"backgroundTag":"llm-empty-completion","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}