fix(tool-output): preserve prompt prefixes by default (#7455)

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Tsai Yuan 2026-10-27 10:09:03 +08:00 committed by GitHub
parent 52f6ff078c
commit 89acf51caa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 59 additions and 23 deletions

View File

@ -1389,6 +1389,11 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**.
### Changed
- **tool output:** Superseded `write_file` payload elision is now opt-in. The
optimization remains available, but the default keeps model-bound history
append-only so later file actions do not invalidate provider prompt-cache
prefixes. ([#7319](https://github.com/bytedance/deer-flow/issues/7319))
- **skills:** Treat `allowed-tools` as non-enforced compatibility metadata.
Activating a skill no longer hides or blocks tools that Agent configuration
and host authorization already permit, including when the declaration is

View File

@ -1106,6 +1106,10 @@
### 变更
- **工具输出:** 被后续操作取代的 `write_file` 载荷省略现在改为显式启用。该优化仍可
使用,但默认会保持发送给模型的历史只追加,避免后续文件操作使 provider 的提示词缓存
前缀失效。([#7319](https://github.com/bytedance/deer-flow/issues/7319))
- **技能:** 将 `allowed-tools` 作为不强制执行的兼容性元数据。激活技能不再隐藏或
阻止智能体配置与宿主授权已经允许的工具,即使该声明为空也是如此。保留解析、校验、
导出、技能激活权限,以及现有的密钥和用户隔离边界。([#6935])

View File

@ -2833,6 +2833,8 @@ An explicit zero per-tool override disables externalization while preserving
any positive global fallback limit.
Structured JSON tool results follow these budgets, including results mixed with
media; externalization keeps the full JSON available through a file reference.
Superseded `write_file` payload elision is opt-in because enabling it rewrites
earlier model-request prefixes when later file actions supersede a write.
MindIE renders JSON tool results as escaped text, with configured tool exemptions
preserved. Enabled PII redaction scans nested JSON keys and values before rendering.
Mounted sandboxes running under another UID need read access through the shared

View File

@ -61,6 +61,8 @@ No match: skip inspection, log ID. Tests: `test_read_mark_tool_call_correlation.
**KnowledgeScopeMiddleware** follows input sanitization: it exposes only Gateway-admitted execution scope, removes scope/display data from model messages, and blocks `knowledge_search`, `knowledge_entries_search` and `entity_relations_search` when disabled without reading storage or RAGFlow.
2. **ToolOutputBudgetMiddleware** - Budgets model-bound text/JSON with limits/exemptions; externalizes to thread outputs (`tool_output.storage_subdir`, default `.tool-results`, `TOOL_RESULTS_DIRNAME`) with synopsis/`read_file` refs. Preserve mixed-result media. Restore blob refs across hosts using the recovering Gateway's normal umask; hash/write failures fall back inline. Internal files bypass workspace/delivery scans. Publication/elision/bash exit marker: [contract](TOOL_OUTPUT_PERSISTENCE.md). Elision: `tool_call_args`; retain newest `keep_recent_writes` writes. The placeholder comes from `WRITE_ELISION_PLACEHOLDER_TEMPLATE` (`constants.py`) and `write_file` rejects a payload that *is* that placeholder (whole payload, up to surrounding whitespace), so an imitated placeholder cannot become a deliverable (#7319). Controls: `elide_superseded_writes`, `superseded_write_min_chars`.
Superseded-write elision is opt-in: enabling it reduces context but rewrites
earlier model-request prefixes when later actions supersede writes (#7319).
Tests: `tests/test_tool_output_synopsis_budget.py`.
3. **ToolResultSanitizationMiddleware** - Neutralizes framework/injection tags (e.g. `<system-reminder>`) and boundary markers in *remote-content* tool results (`web_fetch`/`web_search`/`image_search`/`web_capture`) so attacker-controlled fetched pages cannot forge trusted framework context. Mirrors `InputSanitizationMiddleware`'s user-input guardrail for the other untrusted-content entry point; sits inner of `ToolOutputBudgetMiddleware`. Local tool output (bash/read_file) is left untouched. Scope is a name-based allowlist for the first-party web tools, plus every MCP-sourced tool via its `deerflow_mcp` metadata tag, so an MCP server naming its fetcher `fetch_url` is still covered

View File

@ -78,12 +78,15 @@ writers' files based on a TTL.
## Model-bound write elision
`wrap_model_call` elides successful `write_file` content only in model requests
after a later successful same-path `read_file`, `write_file`, or `str_replace`
(#5328); disk remains the reference. `keep_recent_writes` preserves the newest
writes. Use the shared `tool_call_args` helpers (`pair_tool_call_results` and
argument rewriting). Controls: `elide_superseded_writes` and
`superseded_write_min_chars`.
`wrap_model_call` can elide successful `write_file` content only in model
requests after a later successful same-path `read_file`, `write_file`, or
`str_replace` (#5328); disk remains the reference. The optimization is disabled
by default because applying it after a later action rewrites an earlier request
prefix and reduces provider prompt-cache reuse (#7319). Explicitly enable
`elide_superseded_writes` to accept that trade-off. `keep_recent_writes`
preserves the newest writes. Use the shared `tool_call_args` helpers
(`pair_tool_call_results` and argument rewriting). Controls:
`elide_superseded_writes` and `superseded_write_min_chars`.
The placeholder is framework-owned text that the model can see in its own
history, so it can be imitated. `write_file` therefore rejects a payload that

View File

@ -27,11 +27,12 @@ class ToolOutputConfig(BaseModel):
preview + file reference. If disk persistence is unavailable the
output falls back to head+tail truncation.
The same middleware also budgets the other bulky side of a tool call in
The same middleware can also budget the other bulky side of a tool call in
model-bound requests: the ``content`` argument of a successful
``write_file`` call, once a later read or write of the same path has
made the historical copy redundant with the file on disk
(``elide_superseded_writes``; issue #5328).
made the historical copy redundant with the file on disk. This behavior is
opt-in through ``elide_superseded_writes`` because it rewrites an earlier
request prefix (issues #5328 and #7319).
"""
enabled: bool = Field(
@ -97,13 +98,14 @@ class ToolOutputConfig(BaseModel):
description="Per-tool externalize_min_chars overrides. Keys are tool names, values are char thresholds. Use 0 to disable externalization for a specific tool.",
)
elide_superseded_writes: bool = Field(
default=True,
default=False,
description=(
"Replace the content argument of a successful write_file call with a short placeholder in model-bound "
"Opt in to replacing the content argument of a successful write_file call with a short placeholder in model-bound "
"requests once the same path was read or modified again later in the conversation. After a successful "
"write the file on disk is the source of truth, and the read-before-write gate forces a read_file before "
"the next modification, so the historical copy is redundant with that read. Only the request copy "
"changes: stored message history, receipts, and the run journal keep the original arguments."
"the next modification, so the historical copy is redundant with that read. This is disabled by default "
"because enabling it rewrites an earlier model-request prefix when a later action supersedes the write. "
"Only the request copy changes: stored message history, receipts, and the run journal keep the original arguments."
),
)
superseded_write_min_chars: _OutputLimit = Field(

View File

@ -2440,6 +2440,9 @@ class TestSupersededWriteElision:
@staticmethod
def _middleware(**overrides) -> ToolOutputBudgetMiddleware:
# Elision remains available as an explicit opt-in; behavior-focused
# tests enable it so changing the safe default does not weaken them.
overrides.setdefault("elide_superseded_writes", True)
return ToolOutputBudgetMiddleware(config=ToolOutputConfig(**overrides))
@staticmethod
@ -2708,6 +2711,20 @@ class TestSupersededWriteElision:
assert forwarded is request
def test_default_config_keeps_model_history_append_only(self):
mw = ToolOutputBudgetMiddleware(config=ToolOutputConfig())
w1, r1 = _write("call-1", self.PATH, "x" * 5000)
rd, rr = _read("call-2", self.PATH)
w2, r2 = _write("call-3", self.OTHER, "tiny")
first_request, first_forwarded = self._forward(mw, [w1, r1])
later_request, later_forwarded = self._forward(mw, [w1, r1, rd, rr, w2, r2])
assert first_forwarded is first_request
assert later_forwarded is later_request
assert self._content(first_forwarded, 0) == "x" * 5000
assert self._content(later_forwarded, 0) == self._content(first_forwarded, 0)
def test_middleware_disabled_passes_request_through(self):
mw = self._middleware(enabled=False)
w1, r1 = _write("call-1", self.PATH, "x" * 5000)
@ -2720,7 +2737,7 @@ class TestSupersededWriteElision:
def test_config_defaults(self):
config = ToolOutputConfig()
assert config.elide_superseded_writes is True
assert config.elide_superseded_writes is False
assert config.superseded_write_min_chars == 2000
assert config.keep_recent_writes == 1
@ -2743,7 +2760,7 @@ class TestSupersededWriteElision:
with open(example_path, encoding="utf-8") as f:
data = yaml.safe_load(f)
tool_output = data["tool_output"]
assert tool_output["elide_superseded_writes"] is True
assert tool_output["elide_superseded_writes"] is False
assert tool_output["superseded_write_min_chars"] == 2000
assert tool_output["keep_recent_writes"] == 1
# New user-settable keys are a schema change: the outdated-config warning must fire.

View File

@ -1422,10 +1422,10 @@ tool_search:
#
# The same middleware also elides the `content` argument of successful
# write_file calls from model-bound requests once the same path was read or
# modified again later (issue #5328): the file on disk is then the source of
# truth, and the read-before-write gate forces a read_file before the next
# modification anyway, so the historical copy only costs context. Stored
# history, receipts, and the run journal keep the original arguments.
# modified again later (issue #5328). This optimization is opt-in because a
# later superseding action rewrites an earlier model-request prefix, reducing
# provider prompt-cache reuse (#7319). Stored history, receipts, and the run
# journal keep the original arguments when it is enabled.
tool_output:
enabled: true
@ -1444,10 +1444,11 @@ tool_output:
# tool_overrides:
# web_search: 8000
# bash: 20000
# Replace a successful write_file call's `content` with a short placeholder
# pointing at read_file once a later successful read_file / write_file /
# str_replace of the same path exists. Only the model-bound request changes.
elide_superseded_writes: true
# Opt in to replacing a successful write_file call's `content` with a short
# placeholder pointing at read_file once a later successful read_file /
# write_file / str_replace of the same path exists. Only the model-bound
# request changes, but doing so rewrites an earlier prompt prefix.
elide_superseded_writes: false
# Only elide content at least this many characters long (a character count,
# not tokens: CJK text costs ~3-4x more per character than ASCII). 0 elides
# every non-empty content.