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:
- 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.
- "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:
- 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.
- 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.
- 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.
- 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.
- 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.
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:Two consequences:
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:
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.stop) or a tool call. When the bound is exhausted, fall back to today'sGenerationErrorso a pathological model cannot spin forever.lengthwith empty content — that is the context-overflow case, not truncation-with-progress;max_tokensis 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
Acceptance criteria
finish_reason="length"is continued automatically and delivered as one stitched answer, with visible continuation trace entries.GenerationErrorstill fires when the continuation bound is exhausted (with the cap advice intact).