DEV Community

Cover image for When Your AI Agent App Silently Pauses Without Reasons
Phat (Eric) Pham
Phat (Eric) Pham

Posted on

When Your AI Agent App Silently Pauses Without Reasons

When Your AI Agent Silently Pauses: Understanding finishReason: "tool-calls"

If you are building an AI-native application, your agent is probably not just answering chat messages. It may be inspecting data,
calling tools, controlling a browser, running workflows, or collecting observations before it decides what to do next.

That is where agent behavior becomes much more interesting than a normal LLM response.

We once ran into a strange issue while building a practical agent application. The agent would start normally. The request was accepted. The stream was open. Nothing looked obviously broken.

But then the UI simply paused.

No final answer.
No visible tool activity.
No error message.
No “waiting for approval” state.
No clear signal that the agent wanted to continue.
And our UI still shows the stream button, actually make us confused

From the user’s point of view, the agent just stopped working even the task hasn't done yet. As a result, it truly makes a bad experience, which our team experienced in the building AI Native App

The Part That Fooled Us

In traditional software, failures usually leave evidence: an exception, a timeout, a failed request, a rejected promise, or a
crashed worker.

This issue was different.

The system was quiet. The agent did not produce a final response, but it also did not emit a clear user-visible event that the UI
could render. There was no obvious symptom beyond silence.

The clue only appeared when we inspected the lower-level model/runtime metadata:

  {
    "finishReason": "tool-calls"
  }
Enter fullscreen mode Exit fullscreen mode

At first, that looked like the agent had stopped while trying to use tools.

But the deeper lesson was this:

In an AI agent loop, “stopped” does not always mean failed. Sometimes it means the model handed control back to the runtime, and
your application has to decide what happens next.

What tool-calls Really Means

In AI SDK-style agent loops, the model can finish a generation step because it wants to call a tool.

That is not the same thing as producing a final answer.

A simplified mental model looks like this:

  user task
    -> model thinks
    -> model requests tool call
    -> runtime executes tool
    -> runtime sends tool result back to model
    -> model continues
    -> final answer
Enter fullscreen mode Exit fullscreen mode

The important boundary is here:

  model requests tool call
Enter fullscreen mode Exit fullscreen mode

At that point, the model has completed the current generation step. The runtime is expected to execute the tool, append the tool
result, and continue the loop if allowed.

According to the AI SDK loop-control docs, an agent loop continues until a finish reason other than tool-calls is returned, a
called tool has no execute function, approval is needed, or a stop condition is met.

That means tool-calls is not automatically an error. It is a control-flow signal.

Why This Becomes A Product Bug

The product bug happens when this internal control-flow signal never becomes an application-level state.

For example, imagine this runtime event:

  {
    "finishReason": "tool-calls",
    "stepCount": 10,
    "maxSteps": 10,
    "toolCallCount": 1,
    "toolResultCount": 0
  }
Enter fullscreen mode Exit fullscreen mode

To the runtime, this may be understandable: the model requested a tool call, but the loop reached a limit.

To the user, it looks like nothing happened.

  • running a tool
  • waiting for approval
  • blocked by a step limit
  • waiting for continuation

So the product experience becomes a silent pause.

That was the real issue: not that the model wanted to call a tool, but that this state was invisible to the application.

Treat Finish Reasons As State Machine Events

For production agent apps, finish reasons should not disappear into debug logs. They should feed your application state machine.

A simplified example:

  type AgentState =
    | "running"
    | "running_tool"
    | "needs_continuation"
    | "waiting_for_approval"
    | "completed"
    | "failed";

  function classifyAgentState(event: {
    finishReason?: string;
    stepCount: number;
    maxSteps: number;
    toolCallCount: number;
    toolResultCount: number;
    requiresApproval?: boolean;
  }) {
    if (event.requiresApproval) {
      return "waiting_for_approval";
    }

    if (
      event.finishReason === "tool-calls" &&
      event.stepCount >= event.maxSteps
    ) {
      return "needs_continuation";
    }

    if (
      event.finishReason === "tool-calls" &&
      event.toolCallCount > event.toolResultCount
    ) {
      return "running_tool";
    }

    if (event.finishReason === "stop") {
      return "completed";
    }

    return "running";
  }
Enter fullscreen mode Exit fullscreen mode

The exact states depend on your product. The principle matters more than the enum:

Agent runtime metadata should become product state.

What To Log

When debugging agent pauses, do not log only the final text. Log the agent loop.

At minimum, capture:

  onStepFinish({
    stepNumber,
    finishReason,
    toolCalls,
    toolResults,
  }) {
    logger.info("agent_step_finished", {
      stepNumber,
      finishReason,
      toolCallCount: toolCalls.length,
      toolResultCount: toolResults.length,
    });
  }
Enter fullscreen mode Exit fullscreen mode

Useful fields to log together:

  • finishReason
  • step number
  • max step / stop condition
  • tool call count
  • tool result count
  • tool names
  • tool execution errors
  • approval requirements
  • whether the next model step started

Separately, these fields are just metadata. Together, they explain the agent loop.

Practical Advice For Agent App Builders

If you are building with AI SDK, Mastra, or any framework that wraps model/tool loops, design for tool-call boundaries explicitly.

Do not assume every stream finish means the task is done.

Do not assume silence means failure.

Do not assume a tool call is visible to the user unless your app explicitly emits a UI event for it.

A production agent app should have clear states for:

  • thinking
  • calling a tool
  • waiting for a tool result
  • waiting for approval
  • needing continuation
  • completed
  • failed

This becomes especially important for long-running tasks, browser agents, background jobs, resumable workflows, and multi-step
automation.

Keys takeaway

The surprising lesson was that the agent did not fail in the traditional sense.

It stopped at a valid internal boundary that the product did not understand yet.

Once we treated finishReason: "tool-calls" as a control-flow signal instead of invisible metadata, the silent pause became explainable, observable, and fixable.

AI agents are not single responses. They are loops.

And if you are building a real agent application, the edges of that loop are part of your product.

Short Advertise

You guys can visit our product at https://scoutqa.ai to experience what we has been building, register if you see it helpful and totally free now ^ ^!

References

Top comments (0)