mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-27 13:40:37 +00:00
fix(tool-output): preserve prompt prefixes by default (#7455)
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
52f6ff078c
commit
89acf51caa
@ -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
|
||||
|
||||
@ -1106,6 +1106,10 @@
|
||||
|
||||
### 变更
|
||||
|
||||
- **工具输出:** 被后续操作取代的 `write_file` 载荷省略现在改为显式启用。该优化仍可
|
||||
使用,但默认会保持发送给模型的历史只追加,避免后续文件操作使 provider 的提示词缓存
|
||||
前缀失效。([#7319](https://github.com/bytedance/deer-flow/issues/7319))
|
||||
|
||||
- **技能:** 将 `allowed-tools` 作为不强制执行的兼容性元数据。激活技能不再隐藏或
|
||||
阻止智能体配置与宿主授权已经允许的工具,即使该声明为空也是如此。保留解析、校验、
|
||||
导出、技能激活权限,以及现有的密钥和用户隔离边界。([#6935])
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user