Repository navigation
Headless mode exits 0 mid-task when a model response has finish_reason: "tool_calls" but no tool calls #5140
Description
Describe the bug
In non-interactive mode (-p / --prompt-file, --no-ask-user), the CLI ended a long-running agentic session after one model response that came back empty: finish_reason was "tool_calls" and the API billed 193 completion tokens, but message.tool_calls was missing and content was null. The CLI treated this as a normal end of the task and logged runHeadlessMode: succeeded=true and exiting with code 0, even though the agent was in the middle of driving an Android emulator and had not finished.
Because the exit was clean, our wrapper's retry logic never ran. The session's final outputs were never produced. In our case this was a GitHub Agentic Workflows (gh-aw) run, which ended with no safe outputs. We only found out after the fact.
Turn 63 response (request ID 00000-8dd40ac2-8861-4961-b4dd-12408dad56f5), quoted from the --log-level all log:
{
"id": "msg_011Cg27ptWDQ938VwzsJAMAn",
"object": "chat.completion",
"created": 1791980400,
"model": "claude-sonnet-5-5",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": null, "refusal": null },
"finish_reason": "tool_calls",
"logprobs": null
}
],
"usage": {
"prompt_tokens": 136561,
"completion_tokens": 193,
"total_tokens": 136754,
"completion_tokens_details": { "reasoning_tokens": 0 }
}
}The previous 62 turns all had a populated tool_calls array and usually reasoning_text. Their tool calls were bash commands about the same size as the 193 tokens here. This suggests the tool call was generated but dropped somewhere between the model and the CLI. That part may belong to the Copilot API team, but the CLI is where the silent success happens.
Right after that response, the log shows:
executeHeadlessDirectly completing: operation=prompt, hasError=false, taskBlocked=false, blockedExit=false, ... will return true
runHeadlessMode: operation=prompt, succeeded=true
runHeadlessMode: exiting with code 0
Related, but not the same: #3239 is about text-only turns ending the session. Here the response has no content at all, and its finish_reason contradicts its body.
Affected version
1.0.90
Steps to reproduce the behavior
This is not deterministic, since it depends on the API returning the malformed response. It happened once, after about 30 minutes and 62 successful turns:
- Run
copilot --prompt-file <prompt> --no-ask-user --log-level all --log-dir <dir> --allow-tool ...with a long multi-step task that usesbashand MCP tools heavily. Model:claude-sonnet-5.5. - Let a model response arrive with
finish_reason: "tool_calls", notool_callsandcontent: null. - The CLI exits 0 straight away, with no error and no retry.
Expected behavior
A response whose finish_reason is "tool_calls" but which has no tool calls, or an empty response with completion tokens > 0, is malformed. The CLI should treat it as a transient error: retry the request, or at least nudge the model to continue. If retries run out, headless mode should exit non-zero so automation can tell that the task stopped early.
Additional context
- Linux x86_64 (GitHub-hosted
ubuntu-latestrunner), non-interactive. No terminal emulator; the CLI was spawned by the gh-aw harness. - Session ran 30m 23s and 63 turns: 8.9M input tokens (8.7M cached), 31K output tokens.
- Session ID:
1b556a30-bfaa-48f8-a7ab-274d386b0762 - Request ID of the malformed response:
00000-8dd40ac2-8861-4961-b4dd-12408dad56f5, at 2026-10-14T12:20:00Z
Activity