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 the agent turn's run_model_query, a direct string completion that is semantically empty (no visible text, no tool calls) is rejected before the route is committed as accepted: rejected usage is recorded via record_rejected_tool_loop_cost_usage, the accepted-provider-route commit is skipped, and the typed SemanticEmptyTerminalCompletion propagates. This keeps accepted-context accounting and success telemetry clean when the model produced nothing usable.
Source
Thrown at crates/zeroclaw-runtime/src/agent/turn/execution.rs:89
let (served_provider, served_model) = accepted_route
.as_ref()
.map(|route| (route.provider_ref().to_string(), route.model().to_string()))
.unwrap_or_else(|| (self.provider_name.to_string(), self.model.to_string()));
match result {
Ok(response) => {
// A terminal response without final text or tools was billed
// but cannot be accepted. Keep it out of context-window fill
// and successful response telemetry before returning its typed
// cause to every one-shot caller.
if response.is_semantically_empty_terminal() {
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(SemanticEmptyTerminalCompletion));
}
zeroclaw_providers::dispatch::commit_accepted_provider_route(accepted_route);
// Only a semantically valid result controls accepted context
// usage and successful response telemetry.
if let Some(usage) = response.usage.as_ref() {
crate::agent::cost::record_tool_loop_cost_usage(
&served_provider,
&served_model,
usage,
);
}
Ok(response)
}
Err(error) => {
// Accounted dispatch carries rejected usage on failures in the
// typed Reliable error chain. Keep the original error intact so
// terminal-cause classification remains the provider's source
// of truth.View on GitHub (pinned to 88bb9c8533)
Solutions
- Raise the model's max_tokens / completion budget for tool-loop turns.
- Put a non-reasoning model late in the reliable chain so the loop always has a candidate that produces visible text.
- Confirm the rejected usage shows up in cost reporting (it was recorded as rejected, not accepted); use it to size budgets.
- If it recurs on one provider only, check that provider's response shape (content filtering, beta flags) against a raw dump.
Example fix
# before: reasoning model runs the tool loop with a small budget model = "deepseek-r1" max_tokens = 1024 # after: budget sized past thinking, non-reasoning fallback behind it model = "deepseek-r1" max_tokens = 8192 fallback_models = ["gpt-4o-mini"]
Defensive patterns
Strategy: fallback
Validate before calling
fn response_is_semantically_empty(resp: &ChatResponse) -> bool {
resp.tool_calls.is_empty()
&& resp.text.as_deref().map(|t| strip_think_tags(t).trim().is_empty()).unwrap_or(true)
} Try / catch
match turn::run_model_query(&ctx, request).await {
Ok(resp) => Ok(resp),
Err(e) if e.is::<SemanticEmptyTerminalCompletion>() => {
// rejected usage was already recorded; try the non-reasoning fallback route
ctx.with_model(plain_fallback_model()).run_model_query(request).await
}
Err(e) => Err(e),
} Prevention
- Configure the turn's model chain with a non-reasoning fallback so run_model_query has a candidate that emits text.
- Watch rejected vs accepted usage meters: a rising rejected ratio means budget/model problems, not flakiness.
- Set per-turn max_tokens above the model's reasoning spend before enabling it in the loop.
When it happens
Trigger: run_model_query receives a response whose text is empty/think-only and tool_calls is empty; the guard fires before commit_accepted_provider_route, so cost meters mark usage as rejected and the turn fails with the typed terminal error.
Common situations: Agent loops over reasoning models that think without answering, max_tokens exhaustion in the tool loop, gateway hiccup returning 200 with empty content mid-session.
Related errors
- provider completed without final text or tool calls
- provider completed without final text or tool calls
- provider completed without final text or tool calls
- provider completed without final text or tool calls
- provider completed without final text or tool calls
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/8a1e9445e6d7fa96.
Report an issue: GitHub.