fix(write_file): per-model max_tokens + actionable "already exists" error#757
Conversation
#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>
There was a problem hiding this comment.
💡 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".
| if "opus" in slug or "sonnet" in slug: | ||
| return 128000 |
There was a problem hiding this comment.
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 👍 / 👎.
Two chronic
write_filebugs1.
max_tokenswas a flat 16384 — truncated large writesllm/factory.pycapped every model atDEFAULT_LLM_MAX_TOKENS = 16384(~8× below modern Claude limits). A large singlewrite_filecall (full finding report / recon target model) whose content exceeded the cap truncated mid-argument → the tool-call JSON never closed →contentdropped → write failed. Worse on retries.Fix: per-model resolution via new
_model_max_output_tokens(model):opus*/sonnet*(4.x, 4.6/5)haiku*(4.5)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_TOKENSstill wins as an explicit override. Module comment updated.2.
write_fileon 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 atedit_file, using the virtual (not real engagement) path. Plus strengthened the bash tool-usage prompt (tools/bash/prompt.py) so the agent usesedit_filefor existing files andls/read_filefirst when unsure — explicitly covering the retry case.Verification
pytest test_factory.py test_filesystem.py→ 145 passed (incl. newTestMaxTokensResolution+test_write_exists_error_is_actionable_and_masks_real_path)ruff checkon changed files → cleanbasedpyrighton changed source → 0 errors🤖 Generated with Claude Code