Nan Gao cf556fa9d4
feat(agents): elide superseded write_file payloads from model-bound requests (#5374)
* feat(agents): elide superseded write_file payloads from model-bound requests

Step 2 of #5328. After a successful write_file the file on disk is the source
of truth, and the read-before-write gate forces a read_file before the next
modification of that path, so once a later successful read or write of the
same path exists the historical `content` argument is redundant with it. Long
report-writing runs (append-in-chunks) therefore carried every section twice,
once as the write argument and once as the following read output, until
summarization compacted the whole turn.

- ToolOutputBudgetMiddleware's model-call hooks now replace such superseded
  content with a short deterministic placeholder pointing at read_file, in the
  model-bound request only: state["messages"], checkpoints, receipts, loop
  detection, and the run journal keep the original arguments, and nothing is
  externalized to disk. The newest `keep_recent_writes` successful writes
  (default 1) always stay visible; str_replace payloads are never touched; a
  same-turn read never supersedes (parallel calls run in no fixed order); only
  results stamped deerflow_tool_meta.status == "success" count, so failed,
  gate-blocked, partial, or unstamped writes are never candidates.
- New `tool_output.elide_superseded_writes` (default on),
  `tool_output.superseded_write_min_chars` (default 2000), and
  `tool_output.keep_recent_writes` (default 1); config_version 41 -> 42 in
  config.example.yaml and the Helm chart.
- The per-occurrence call/result pairing the gate introduced in #5329 moves
  into the shared `tool_call_args.pair_tool_call_results` helper so both
  policies pair the same way; the gate now uses it.

* fix(agents): scope tool-call result pairing to the issuing turn

Review finding on #5374 (P2): pair_tool_call_results consumed results from a
history-wide per-id queue, so an interrupted write_file with no result whose
tool-call id a later turn reused inherited that later call's success. With
the default elision the unconfirmed draft was then replaced by a placeholder
claiming the write succeeded, and the gate's blocked-call pairing had the
mirror-image hole.

Pair results the way DanglingToolCallMiddleware does: walk in document order,
open each AIMessage's calls, and let a ToolMessage answer only a still-open
call of the most recent preceding AIMessage. A result never answers a call
from an earlier turn, so the interrupted call stays unanswered (never a
candidate, never labeled blocked) and stray or duplicate results are ignored.
Regressions cover the helper, the superseded-write policy, and the gate.

* fix(agents): never rewrite tool-call ids duplicated within one AIMessage

Review finding on #5374 (P2): the policies select calls per occurrence, but
every provider surface is addressed by tool-call id, so when a malformed
provider payload repeats an id inside one assistant turn the rewriter could
only replace all of its occurrences at once. A failed write_file sibling then
took on the superseded successful call's path and elided content and was
presented as a success; the gate's blocked-call elision had the mirror-image
hole (a successful sibling rewritten into the blocked call).

rewrite_messages_tool_call_args now never offers an id that repeats within
its message to the selector and leaves those calls untouched on every
surface. Both policies are covered by the shared helper; regressions cover
the helper, the superseded-write policy, and the gate.

* fix(agents): skip unhashable tool-call ids in the duplicate-id guard

Review finding on #5374 (round 3): _duplicated_call_ids fed every id into a
Counter before the string guard, so a list or dict id from a malformed
provider payload raised TypeError out of wrap_model_call and failed the whole
model call whenever the history also held a rewrite candidate. The pre-PR
loop and pair_tool_call_results skip such ids; only this helper regressed.

Count non-empty string ids only, and pin it with regressions for the helper,
the superseded-write policy, and the gate.

* docs(agents): keep middleware guidance within size limit

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-13 18:04:51 +08:00
..