Skip to content

Auto-continue responses truncated at the output-token limit (finish_reason="length") #345

Description

@furgalep

Problem

When a model hits the output-token limit mid-reply, the provider returns finish_reason="length" together with the partial text produced so far. NOOA currently treats this as a terminal error:

# src/nooa/strategies/codeact.py:1010-1024
if response.finish_reason == "length":
    session.record_error()
    ...
    turn_state.is_final = True
    raise GenerationError(
        "The model used all available output tokens before completing "
        "a tool call. Increase `max_tokens` (16384 or more is often "
        "needed for reasoning models such as GPT-5.5 and o-series)."
    )

Two consequences:

  1. Usable partial text is discarded. A response that stopped mid-sentence often contains most of the answer — long reviews, multi-part plans, and other analytical deliverables are precisely the responses most likely to exceed a cap. The turn ends in an error and the user must re-prompt from scratch, paying for the partial generation again.
  2. "Increase max_tokens" is not always a remedy. Some routes and gateways cap output regardless of the requested value, and reasoning models consume the same budget with thinking tokens, so the effective answer budget can be far smaller than configured.

(The token-budget summary fork correctly rejects length-truncated summaries — src/nooa/agents/summarization.py:750 — because a partial summary must never become a checkpoint. That behavior should stay as-is. The gap is the main conversation path, which has no recovery at all.)

Proposed solution

Bounded auto-continuation in the agent loop:

  1. When a terminal assistant response has finish_reason == "length" and non-empty text content, append an internal continuation prompt (e.g. "Continue from exactly where you left off. Do not repeat text already produced.") and run another generation, preserving the partial text so the segments can be stitched into the final answer.
  2. Bound it: at most 3 consecutive length-continuations per turn; the counter resets on natural completion (stop) or a tool call. When the bound is exhausted, fall back to today's GenerationError so a pathological model cannot spin forever.
  3. Exclusions (keep today's behavior for these):
    • length with empty content — that is the context-overflow case, not truncation-with-progress;
    • responses whose only content is tool calls — already actionable, the loop continues after executing them;
    • disposed sessions and pending user steering.
  4. Make it visible: one debug-trace line per continuation, plus a note on the final response when segments were stitched, so the user can see the reply was continued rather than delivered whole.
  5. Emit a metric for length-continuations per model — a high rate signals that the configured max_tokens is too small for that model, and the existing error message's cap advice remains the right steady-state fix for users.

The decision logic (continue / exclude / bound-reached) belongs in a small pure helper next to the CodeAct length branch so it is unit-testable independently of the loop.

Why continuation instead of only raising the cap

  • Provider-agnostic: works even when the route caps output below the requested value.
  • Complements, not replaces, the cap advice: cap tuning fixes the steady state; continuation recovers the in-flight turn without re-generating the whole answer.
  • Bounded and observable: each retry is one extra generation, visible in the trace — unlike silent cap inflation.

Acceptance criteria

  • A text response truncated at finish_reason="length" is continued automatically and delivered as one stitched answer, with visible continuation trace entries.
  • The existing terminal GenerationError still fires when the continuation bound is exhausted (with the cap advice intact).
  • Empty-content and tool-call-only length responses keep today's behavior unchanged.
  • Unit tests for the decision helper (continue / each exclusion / bound exhaustion / counter reset) and an integration test stitching two partial segments.

Activity

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

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions