#2117

[BUG] write_to_file: required `title` is shadowed by the injected `message_title`; a missing argument aborts at argument binding yet the Run is failed as "may have produced an external side effect"

Openllanozeroopened 14d ago

Problem description

Provenance & confidence (please read first)

Produced with an AI-assisted investigation. Partly reproduced: the production error text and the zero-side-effect outcome are quoted from our journal and confirmed locally; but note the local direct call raises unexpected keyword argument 'message_title' rather than the production missing 1 required positional argument: 'title', because ToolkitMessageIntegration pops the injected keys before invoking the function (message_integration.py:101). Both are argument-binding failures that happen before the function body runs — that equivalence is code reading, please confirm it in your dispatch path. The claim that the Run is failed with outcome_unknown is taken directly from our tool_calls row and runs.terminal_detail, not inferred.

FileToolkit.write_to_file is re-wrapped in Eigent with a positional, defaultless title:

backend/app/agent/toolkit/file_write_toolkit.py:141
    lambda _, title, content, filename, encoding=None, use_latex=False: (

At the same time, ToolkitMessageIntegration injects message_title / message_description into the same advertised parameter list, documented as an optional status-message title (camel/toolkits/message_integration.py:157, :535).

The two names are near-synonyms, one required and one optional. A model that fills the injected cosmetic field and omits the real one produces:

Error: FileToolkit.<lambda>() missing 1 required positional argument: 'title'

Observed request (our journal, tool_calls.request_json, abridged):

{"content": "…", "filename": "write-test/probe.csv", "encoding": "utf-8",
 "message_title": "Fixing CSV format", "message_description": "…", "use_latex": false}
# no "title"

Two consequences worth separating:

  1. The failure itself is a plain argument-binding error: no tool code executed, no file was created (verified: target path absent, git status empty).
  2. The classification is wrong in proportion. Because the exception surfaces after dispatch has begun and the tool is unsafe_write, run_runtime/tool_checkpoint.py:940-951 records status = "outcome_unknown" and external_effect_may_have_occurred = true, which fails the entire Run (terminal_detail = "execution_backend_failure: tool 'write_to_file' may have produced an external side effect") — 0.8 s after dispatch, with timeout_reason = NULL.

So the cheapest, provably side-effect-free error is punished with the heaviest outcome, while a genuine write failure (e.g. RepositoryStateChangedError) only costs one call.

Steps to reproduce

  1. Git-enabled Space, single-agent.
  2. Ask the agent to write a file, and have the call omit title (or send only message_title).
  3. Observe: the tool raises a Python binding error; the tool call is journalled outcome_unknown; the Run is marked failed with an "external side effect" reason; no file exists.

Additional context

Suggested fixes, any one of which removes the trap:

  1. Give title a default in the wrapper and fall back to the injected message_title (or better, remove the near-synonym collision: rename the injected field, or suppress it for tools that already have a required title).
  2. Classify argument-binding failures distinctly from post-effect ambiguity. Python raises TypeError: … missing 1 required positional argument / unexpected keyword argument before the function body runs, so absence of external effect is provable. A fail-closed boundary is right, but it should not treat "proven not to have happened" the same as "cannot prove whether it happened".
  3. Related family we hit the same day (same root shape — model-produced argument structure vs a schema that permits near-synonyms): shell_view(id='t6', block='false') where block belongs to shell_exec; a call to a non-existent tool name write_file; and file_paths arriving as a stringified JSON array, which markitdown then treats as one literal path (File not found: <HOME>/workbench/["write-test/probe.txt", …). A stricter server-side argument validation step — rejecting unknown/missing required keys with a clean, retryable tool error rather than an ambiguous one — would turn all four of these into ordinary recoverable failures.

Activity

0

No activity yet.