Skip to content

Headless mode exits 0 mid-task when a model response has finish_reason: "tool_calls" but no tool calls #5140

Open

Description

@MattSkala

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:

  1. Run copilot --prompt-file <prompt> --no-ask-user --log-level all --log-dir <dir> --allow-tool ... with a long multi-step task that uses bash and MCP tools heavily. Model: claude-sonnet-5.5.
  2. Let a model response arrive with finish_reason: "tool_calls", no tool_calls and content: null.
  3. 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-latest runner), 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:non-interactiveNon-interactive mode (-p), CI/CD, ACP protocol, and headless automation

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions