Skip to content

fix(write_file): per-model max_tokens + actionable "already exists" error#757

Merged
PurpleCHOIms merged 3 commits into
mainfrom
fix/write-file-max-tokens-and-exists-guidance
Jul 8, 2026
Merged

fix(write_file): per-model max_tokens + actionable "already exists" error#757
PurpleCHOIms merged 3 commits into
mainfrom
fix/write-file-max-tokens-and-exists-guidance

Conversation

@PurpleCHOIms

Copy link
Copy Markdown
Member

Two chronic write_file bugs

1. max_tokens was a flat 16384 — truncated large writes

llm/factory.py capped every model at DEFAULT_LLM_MAX_TOKENS = 16384 (~8× below modern Claude limits). A large single write_file call (full finding report / recon target model) whose content exceeded the cap truncated mid-argument → the tool-call JSON never closed → content dropped → write failed. Worse on retries.

Fix: per-model resolution via new _model_max_output_tokens(model):

Model family Max output
opus* / sonnet* (4.x, 4.6/5) 128000
haiku* (4.5) 64000
unknown / non-Claude 64000 (safe default)

Matched on the model slug suffix so every namespace resolves the same (anthropic/claude-opus-4-8, auth/claude-opus-4-8, openrouter/anthropic/claude-sonnet-4-6). It is a ceiling, not a forced value — short replies cost nothing extra; values >64k use the streaming path the SDK already takes. DECEPTICON_LLM_MAX_TOKENS still wins as an explicit override. Module comment updated.

2. write_file on an existing path → noisy "already exists"

The agent wrote a path that existed → error → it switched to edit, producing chronic error noise (and a Section-D retry re-writing a first-attempt file).

The refusal is raised in the deepagents backends (deepagents/backends/{state,filesystem,sandbox}.py) — an external dep, no-overwrite by design (overwriting would force the agent to re-emit the whole file for a one-line change → wasteful + re-triggers the max_tokens truncation). We keep no-overwrite.

Fix: EngagementFilesystemBackend.write (OSS-controlled choke point through which every red-team filesystem write routes) now normalizes the varying backend messages (terse "File already exists" from sandbox, longer one from state/filesystem) into one actionable instruction that points at edit_file, using the virtual (not real engagement) path. Plus strengthened the bash tool-usage prompt (tools/bash/prompt.py) so the agent uses edit_file for existing files and ls/read_file first when unsure — explicitly covering the retry case.

Verification

  • pytest test_factory.py test_filesystem.py145 passed (incl. new TestMaxTokensResolution + test_write_exists_error_is_actionable_and_masks_real_path)
  • ruff check on changed files → clean
  • basedpyright on changed source → 0 errors

🤖 Generated with Claude Code

PurpleCHOIms and others added 2 commits July 3, 2026 14:19
#749 made the filesystem middleware re-resolve the per-run sandbox via
build_sandbox_backend(config) — i.e. it now calls build_sandbox_backend with a
positional `config` argument. The rebind unit tests still patched it with a
zero-arg `lambda: per_run`, so every call raised

  TypeError: <lambda>() takes 0 positional arguments but 1 was given

turning three tests red on main (test_rebind_reresolves_bare_http_sandbox_per_run,
test_rebind_swaps_composite_default_preserving_routes,
test_get_backend_reresolves_sandbox_transport_per_run) — a stale mock, not a
production bug.

Mirror the real signature build_sandbox_backend(config=None) in the four mocks
(the "must not be called" throw-mock included for consistency). Target tests:
5 passed. ruff format + check clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rror

Two chronic write_file failures:

1. Output was capped at a flat 16384 tokens (DEFAULT_LLM_MAX_TOKENS), ~8x
   below modern Claude limits. A large single write_file call (full finding
   report / recon model) truncated mid-argument, the tool-call JSON never
   closed, `content` was dropped, and the write failed. Now each model gets
   its OWN max-output ceiling: opus*/sonnet* -> 128000, haiku* -> 64000,
   unknown -> safe 64000 default. It is a ceiling (short replies cost
   nothing extra); values >64k use the streaming path the SDK already takes.
   DECEPTICON_LLM_MAX_TOKENS still wins as an explicit override.

2. write_file on an existing path errored, and the agent had to guess to
   switch to edit_file. The refusal is raised in the deepagents backends
   (external dep, no-overwrite by design — an overwrite would force the
   agent to re-emit the whole file for a one-line change, wasteful and it
   re-triggers the max_tokens truncation). Left as no-overwrite; instead
   EngagementFilesystemBackend.write (OSS-controlled choke point) now
   normalizes the varying backend messages into one actionable instruction
   pointing at edit_file, with the virtual (not real) path. Strengthened the
   bash tool prompt so the agent uses edit_file for existing files and
   ls/read first when unsure, covering retries that re-write a file the
   first attempt already created.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 77778f1d27

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +95 to +96
if "opus" in slug or "sonnet" in slug:
return 128000

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict 128k caps to supported Claude versions

This substring check gives max_tokens=128000 to any model slug containing opus or sonnet, including valid dynamic/fallback routes for older Claude models such as anthropic/claude-sonnet-4-5 or gateway aliases with the same slug. Anthropic's current docs list 128k output only for Mythos Preview, Opus 4.8/4.7/4.6, Sonnet 5, and Sonnet 4.6, while Haiku 4.5 is 64k (https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking); unsupported 128k values are sent on every request and can make those configured models fail before generating. Match the specific versioned slugs instead of every opus/sonnet.

Useful? React with 👍 / 👎.

@PurpleCHOIms PurpleCHOIms merged commit 2bc249a into main Jul 8, 2026
25 checks passed
@PurpleCHOIms PurpleCHOIms deleted the fix/write-file-max-tokens-and-exists-guidance branch July 8, 2026 06:43
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.

1 participant