Skip to content

Think: an auto-continuation that fails before streaming is only logged — no onChatError, no response hook, no terminal status #2381

Closed

Description

@zoujiong-github

Versions

@cloudflare/think@0.19.0, agents@0.24.0, ai@7.0.113, Workers compatibility date 2026-09-13.

Summary

When the auto-continuation that runs after a client tool-approval response (or tool result) throws before it produces a stream — for example from beforeTurn() or from beforeStep() at step 0 — Think only writes console.error("[Think] Auto-continuation failed:", error). None of the application-facing error paths run:

  • onChatError() is not called,
  • onChatResponse() is not called (no status: "error" result),
  • no terminal chat status is recorded (_recordTerminalChatStatus), so nothing is replayed on reconnect,
  • the connected client only receives STREAM_RESUME_NONE.

The browser therefore shows no error at all: the approved tool call stays at approval-responded and the chat looks idle. Application resources tied to the turn (leases, reservations released in onChatError) are also left unsettled.

The same kind of failure in a client-submitted turn, or once a continuation is streaming, does reach onChatError / onChatResponse.

Where

dist/think.js, _fireAutoContinuation():

this._admitTurn({
  admission: "queue",
  trigger: "auto-continuation",
  requestId,
  continuation: true,
  allowNested: true,
  execute: async () => {
    // ...
    try {
      // _runInferenceLoop(...) then _streamResult(...) — throws here before any stream
    } finally {
      this._aborts.remove(requestId);
      if (!streamed) this._continuation.sendResumeNone();
      this._continuation.clearPending();
      this._activateDeferredContinuation();
    }
  }
}).catch((error) => {
  console.error("[Think] Auto-continuation failed:", error);   // <- the only handling
  this._aborts.remove(requestId);
});

Reproduction

  1. A Think subclass with an approval-required tool (needsApproval: true, or an inline-approval Action), and a beforeStep() that throws when a flag is set:

    class ReproAgent extends Think<Env> {
      failNextStep = false;
      async beforeStep() {
        if (this.failNextStep) throw new Error("repro_pre_stream_failure");
      }
      // getTools(): one tool with needsApproval: true
    }
  2. From the client (useAgentChat), send a message that makes the model call the approval-required tool; the approval card appears.

  3. Set failNextStep = true on the Agent (e.g. via a callable), then approve the tool call (addToolApprovalResponse).

  4. Think schedules the auto-continuation; beforeStep() throws at step 0.

Observed: the Worker logs [Think] Auto-continuation failed: Error: repro_pre_stream_failure (plus the Task failure and the Agent's default onError output). onChatError and onChatResponse are never called; the client shows no error and the tool part stays approval-responded. Reconnecting shows nothing either.

Expected: the same handling as a failed client turn — onChatError(error, { stage: "turn", ... }), a terminal chat status recorded for replay, and onChatResponse({ status: "error", continuation: true, ... }) — so applications can settle resources and the client can show the failure.

Workaround we use

We mark continuation turns in beforeTurn() (context.continuation), catch errors thrown from beforeTurn() or from beforeStep() at stepNumber === 0, call our own onChatError() for those, and publish the failure to clients through Agent state. We would like to remove this once Think reports these failures itself.

Related

Because the continuation never runs, the approved call is also never settled; see the companion issue about repairInterruptedToolParts keeping approval-responded parts, which then makes every later turn fail at the provider.

Activity

  1. Momoyeyu commented on Sep 28, 2026

    @Momoyeyu

    I can take this. I reproduced the pre-stream failure paths in beforeTurn and beforeStep locally and have a focused fix with Workers regression tests. I will send a PR against main.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions