Skip to content

Commit 2251856

Browse files
authored
Merge pull request #66 from D41910/fix/swarm-gate2-follows-exec-cap
fix(agent-team): let gate ② track the configurable gate ① for bash / run_python_code
2 parents 4b94fbe + f88592d commit 2251856

2 files changed

Lines changed: 82 additions & 8 deletions

File tree

‎tests/test_tool_result_truncation.py‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,3 +492,64 @@ def test_site3_keeps_the_self_pagination_pointer(processor, tool: str) -> None:
492492
# work instead of being handed the path or offset that continues it.
493493
assert "/spill/abc123.md" in out, f"{tool}: recovery pointer was cut"
494494
assert out == body, f"{tool}: body was shortened despite fitting gate ①"
495+
496+
497+
_EXEC_TOOLS = ["bash", "run_python_code"]
498+
499+
500+
@pytest.mark.parametrize("processor", _processors())
501+
@pytest.mark.parametrize("tool", _EXEC_TOOLS)
502+
def test_the_exec_pointer_survives_the_default_gate_1(
503+
processor, tool: str, spill_store
504+
) -> None:
505+
"""gate ② must not cut below the gate ① preview it is protecting.
506+
507+
``bash``/``run_python_code`` overflow to disk at gate ①
508+
(``meta.max_result_chars`` = ``tool_exec_result_max_chars``, 8K by default)
509+
and the preview that comes back carries both the recovery pointer and, for
510+
bash, the exit status. Those live at the tail. If gate ②'s budget for these
511+
tools is a hardcoded number below gate ①, it takes the pointer and the
512+
verdict with it and the agent is left with a truncated result it cannot
513+
recover — the exact failure the 10K grep/glob entries above exist to prevent,
514+
one layer down.
515+
"""
516+
from frontier_agent.core.loop_types import ToolResult
517+
from frontier_agent.infra.config import get_config
518+
from plugins.tools import _overflow
519+
520+
cap = get_config().tool_exec_result_max_chars
521+
body = _overflow.maybe_overflow(tool, _pytest_output(cap * 3))
522+
assert "saved read-only at" in body, f"{tool}: fixture did not overflow"
523+
524+
out = processor.process(ToolResult(
525+
name=tool, args={}, result=body, duration_ms=0,
526+
tool_call_id="call-1", is_error=False,
527+
))
528+
529+
assert "saved read-only at" in out, f"{tool}: gate ② cut gate ①'s recovery pointer"
530+
assert "1 failed, 812 passed" in out, f"{tool}: gate ② cut gate ①'s verdict"
531+
532+
533+
@pytest.mark.parametrize("processor", _processors())
534+
@pytest.mark.parametrize("tool", _EXEC_TOOLS)
535+
def test_gate_2_follows_a_raised_exec_cap(processor, tool: str, monkeypatch) -> None:
536+
"""Raising ``TOOL_EXEC_RESULT_MAX_CHARS`` has to widen gate ② too.
537+
538+
The cap is documented as tunable (docs/tool-result-truncation-ab.md runs the
539+
A/B at 8000 vs 50000). A deployment that raises it to give long scans more
540+
room must actually get more room; a fixed gate ② would silently cap the gain
541+
at its own hardcoded number.
542+
"""
543+
from frontier_agent.core.loop_types import ToolResult
544+
from frontier_agent.infra.config import get_config
545+
546+
monkeypatch.setattr(get_config(), "tool_exec_result_max_chars", 50_000)
547+
body = _pytest_output(40_000)
548+
549+
out = processor.process(ToolResult(
550+
name=tool, args={}, result=body, duration_ms=0,
551+
tool_call_id="call-1", is_error=False,
552+
))
553+
554+
# 50K + 2K headroom covers the 40K body, so nothing is cut.
555+
assert out == body, f"{tool}: a raised gate ① did not widen gate ②"

‎workflows/agent_team/subagent_runtime.py‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
compaction_trigger_tokens,
5959
)
6060
from frontier_agent.core.runtime.loop.tool_exec import ToolResultPostProcessor
61+
from frontier_agent.infra.config import get_config
6162
from frontier_agent.state.event_store.sqlite import EventStore
6263
from plugins.tools._filesystem_state import current_filesystem_state
6364
from plugins.tools.bash import BASH_STDERR_SEPARATOR
@@ -741,13 +742,6 @@ class SwarmToolResultPostProcessor(ToolResultPostProcessor):
741742
})
742743

743744
_BUDGETS: ClassVar[dict[str, int]] = {
744-
"bash": 4_000,
745-
"run_python_code": 5_000,
746-
# Gate ① overflows these to disk at meta.max_result_chars=8K and puts the
747-
# "saved to <path>, use read_file" pointer at the TAIL. The 6K default cut
748-
# below that and took the pointer with it, leaving a truncated result with
749-
# no way back to the rest. 10K leaves the 8K body and its footer room to
750-
# both survive — the value react has carried for this reason all along.
751745
"grep_search": 10_000,
752746
"glob_search": 10_000,
753747
# Belt-and-suspenders: collect_reports is whitelisted above (pass-
@@ -756,6 +750,17 @@ class SwarmToolResultPostProcessor(ToolResultPostProcessor):
756750
# TOOL_RESULT_MAX_CHARS ceiling) rather than falling back to 6K.
757751
"collect_reports": 150_000,
758752
}
753+
# bash / run_python_code overflow to disk at gate ①, which is configurable
754+
# via TOOL_EXEC_RESULT_MAX_CHARS, and they carry two things worth keeping:
755+
# the "saved to <path>, use read_file" pointer and the exit status, both of
756+
# which live at the tail. Gate ② must therefore track gate ① — a deployment
757+
# that raises the exec cap to give long scans more room would otherwise have
758+
# gate ② cut the extra room straight back off, and a deployment that raises
759+
# it above the fixed 6K default would lose the recovery path entirely. Same
760+
# rule, same headroom, as the React processor in
761+
# workflows.stateful_react_agent._runtime.
762+
_CONFIGURED_EXEC_TOOLS = frozenset({"bash", "run_python_code"})
763+
_EXEC_FOOTER_HEADROOM = 2_000
759764
_BUDGET_DEFAULT = 6_000
760765

761766
_BASH_STDERR_KEEP = 3_000
@@ -777,7 +782,15 @@ def process(self, tool_result: ToolResult) -> str:
777782
if name in self._PASS_THROUGH:
778783
return content
779784

780-
budget = self._BUDGETS.get(name, self._BUDGET_DEFAULT)
785+
if name in self._CONFIGURED_EXEC_TOOLS:
786+
# Gate ① is configurable. Gate ② must track it or a deployment
787+
# raising TOOL_EXEC_RESULT_MAX_CHARS can lose the overflow pointer.
788+
budget = (
789+
get_config().tool_exec_result_max_chars
790+
+ self._EXEC_FOOTER_HEADROOM
791+
)
792+
else:
793+
budget = self._BUDGETS.get(name, self._BUDGET_DEFAULT)
781794
if name == "bash":
782795
return self._compact_bash(content, budget)
783796
return self._head_cap(content, budget)

0 commit comments

Comments
 (0)