Skip to content

feat: default per-stage output caps as shares of the context window - #68

Merged
siarheidudko merged 3 commits into
mainfrom
claude/output-caps
Nov 9, 2026
Merged

siarheidudko merged 3 commits into
mainfrom
claude/output-caps

Conversation

@siarheidudko

Copy link
Copy Markdown
Member

Why

Node sent no maxOutputTokens unless limits.perCall was set, so a single call could write up to the provider's maximum, and the browser package behaved differently. Agreed in the thread: derive defaults from the context window, with a floor and a ceiling; an explicit limits.perCall keeps winning.

What

  • Defaults — a share of compaction.contextWindowTokens (default 128k), between a floor and a ceiling, never more than half of a small window:

    Stage Share Floor – ceiling At 128k
    planner 1/64 512 – 4,000 2,000
    executor 1/16 2,048 – 32,000 8,000
    replanner 1/128 256 – 2,000 1,000
    synthesizer 1/32 1,024 – 16,000 4,000

    The executor is the widest because a tool call's arguments (a file it writes) count as output. The numbers differ from the browser's (800 / 1,200 / 400 / 3,000) on purpose: Node had no cap at all, so the defaults are generous.

  • limits.perCall wins; 0 is an explicit "no cap" (the previous behaviour).

  • A stage that thinks gets no default cap. On OpenAI and Gemini reasoning counts against the output cap, so a default sized for the answer alone could leave a thinking model no room to write it.

  • A cut-off call says so. Executor / synthesizer emit a log warning naming the cap and how to lift it; a plan or replanner decision truncated by the cap fails with the reason appended (same error object, so retry behaviour is unchanged). A default cap otherwise truncates silently.

  • CLI: AGENT_PER_CALL_TOKENS (JSON, validated eagerly), in --help, env.example and the README.

Behaviour change

Runs that relied on no per-call cap now get one. Set perCall.<stage>: 0 (or AGENT_PER_CALL_TOKENS={"executor":0}) to restore the old behaviour. Called out in the README.

Checks

typecheck, format:check, build, test green (409 pass, 1 skipped = live model). New tests/output-caps.test.ts covers the numbers, windows from 2k to 1M, precedence, 0, thinking per stage, the notices, and the real wire (max_tokens on the scripted OpenAI endpoint, incl. finish_reason: length). 21 mutations of the cap logic and 6 of the env parser are all caught. Not run against a live provider.


Generated by Claude Code

Until now Node sent no maxOutputTokens unless limits.perCall was set, so one call
could write up to the provider's maximum. Each stage now gets a share of
compaction.contextWindowTokens between a floor and a ceiling (128k: planner 2000,
executor 8000, replanner 1000, synthesizer 4000), like the browser package.

- limits.perCall still wins, and 0 means no cap.
- A stage that thinks gets no default: reasoning counts against the cap on OpenAI
  and Gemini, so a default sized for the answer would starve the answer.
- A call the cap cuts off is reported: a log warning for the executor and
  synthesizer, the reason appended to a plan / replanner parse failure.
- AGENT_PER_CALL_TOKENS (JSON) sets the caps from the CLI.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FKop4At26QqqkwVGEjJur
@siarheidudko
siarheidudko marked this pull request as ready for review November 9, 2026 08:43
@siarheidudko
siarheidudko merged commit 6e5115d into main Nov 9, 2026
9 checks passed
@siarheidudko
siarheidudko deleted the claude/output-caps branch November 9, 2026 08:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants