Skip to content

fix(agent-team): let gate ② track the configurable gate ① for bash / run_python_code - #66

Merged
zhanghanduo merged 1 commit into
ApodexAI:mainfrom
D41910:fix/swarm-gate2-follows-exec-cap
Oct 13, 2026
Merged

zhanghanduo merged 1 commit into
ApodexAI:mainfrom
D41910:fix/swarm-gate2-follows-exec-cap

Conversation

@D41910

@D41910 D41910 commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Fixes a gate ① / gate ② budget drift in the agent_team tool-result post-processor.

bash and run_python_code overflow to disk at gate ①, whose cap is
meta.max_result_chars = TOOL_EXEC_RESULT_MAX_CHARS (8K by default, and the
knob docs/tool-result-truncation-ab.md actually turns between 8000 and 50000).
The preview that comes back carries two things at its tail: the
saved read-only at <path> pointer and the verdict (exit status, the pytest
summary line).

SwarmToolResultPostProcessor gave those two tools hardcoded gate ② budgets
of 4K and 5K — below gate ①'s own preview:

_BUDGETS: ClassVar[dict[str, int]] = {
    "bash": 4_000,
    "run_python_code": 5_000,
    ...

Two consequences, both reproduced against the real code path:

1. At the default 8K cap, run_python_code already loses both. A 137,967-char
result overflows to a 7,930-char preview naming the spill file; gate ② then cuts
it to 5,116 chars and the pointer and the verdict are gone. bash survives
only by accident — _compact_bash happens to keep the tail.

2. Raising the cap buys an agent_team deployment nothing. At 50K, gate ①
returns a 49,933-char preview and gate ② hands the model 4,037 (bash) or 5,116
(run_python_code). The knob only widens the window between two caps.

### TOOL_EXEC_RESULT_MAX_CHARS=50000  tool=run_python_code
  gate(1) cap=50000  result=137967 -> preview 49933  spilled=True
    gate(2) React  (stateful_react):  49933 chars | recovery path: SURVIVES | '1 failed' verdict: kept
    gate(2) Swarm  (agent_team)   :   5116 chars | recovery path: *** LOST *** | '1 failed' verdict: *** LOST ***

Why this is an omission, not a decision

ReactToolResultPostProcessor in workflows/stateful_react_agent/_runtime.py
already carries the rule, with the reasoning written down:

if name in self._CONFIGURED_EXEC_TOOLS:
    # Gate ① is configurable. Gate ② must track it or a deployment
    # raising TOOL_EXEC_RESULT_MAX_CHARS can lose the overflow pointer.

Three further signs in this file that the two tools were expected to follow the
config, not a literal:

  • the grep_search / glob_search entries right above were raised to 10K for
    exactly this reason ("Gate ① overflows these to disk at meta.max_result_chars=8K
    and puts the 'saved to , use read_file' pointer at the TAIL … 10K leaves
    the 8K body and its footer room to both survive — the value react has carried
    for this reason all along");
  • test_site3_keeps_the_self_pagination_pointer is parametrised over both
    processors precisely so "a future divergence fails here instead of in a
    benchmark trajectory" — but its tool list covers only read_file,
    grep_search, glob_search;
  • _compact_bash's docstring already warns that the split "silently degrades to
    'treat the whole blob as stdout'" if the producers change — this budget is the
    other half of that same contract.

So the two processors disagree on exactly the tools whose gate ① is configurable.

The fix

Wire in the same rule the React processor uses — one import, two constants, one
branch:

_CONFIGURED_EXEC_TOOLS = frozenset({"bash", "run_python_code"})
_EXEC_FOOTER_HEADROOM = 2_000
...
if name in self._CONFIGURED_EXEC_TOOLS:
    budget = get_config().tool_exec_result_max_chars + self._EXEC_FOOTER_HEADROOM
else:
    budget = self._BUDGETS.get(name, self._BUDGET_DEFAULT)

The _ELLIPSIS wording, the stderr split and the 3K stderr keep window are all
left alone.

Tests

Added to the existing tests/test_tool_result_truncation.py (no new file),
parametrised over both processors via the file's existing _processors()
helper — 4 new cases per processor:

  • test_the_exec_pointer_survives_the_default_gate_1 — a gate-①-sized overflow
    must keep both the spill path and the verdict at the default cap.
  • test_gate_2_follows_a_raised_exec_cap — with the cap at 50K, a 40K body must
    come through uncut, i.e. raising the knob actually widens gate ②.

Validity check — reverting only subagent_runtime.py and keeping the new
tests fails exactly 3:

FAILED test_the_exec_pointer_survives_the_default_gate_1[run_python_code-agent_team]
FAILED test_gate_2_follows_a_raised_exec_cap[bash-agent_team]
FAILED test_gate_2_follows_a_raised_exec_cap[run_python_code-agent_team]
3 failed, 38 passed

Every failure is the agent_team parameter. The stateful_react parameters pass
both before and after — that is the shape of an omission, not a redesign.

Differential fuzz, 2016 cases (12 tools × 4 body shapes × 21 sizes ×
error flag), same probe before and after via git stash:

total 2016 | unchanged 1891 | changed 125
changed by tool: {'bash': 64, 'run_python_code': 61}
VIOLATIONS: 0
  • no non-exec tool changed at all (read_file / grep_search / glob_search /
    web_fetch / web_search / collect_reports / delegate_subtask / submit_report /
    unknown / empty-name: 0 changes);
  • no is_error=True case changed — errors still bypass every cap;
  • the React processor's output is byte-identical in all 2016 cases.

Verification

check result
pytest tests/test_tool_result_truncation.py 41 passed (33 before)
pytest tests/ (full, 3896 collected) 3886 passed, 10 skipped, 1 failed
ruff check (CI's lint gate) clean
pyright 0 errors, 0 warnings

The single full-suite failure is tests/test_network_tools.py::test_redirect_hops_are_validated_not_followed_blindly
— this sandbox resolves www.iana.org to 198.18.0.44, so the URL guard refuses
it. It fails identically on unmodified main (git stash → same 1 failed,
17 passed in that file), so it is not from this change.

ruff format --check reports 2 files needing reformatting, also identically on
unmodified main
(this machine's ruff is newer than the repo's pin; the hunks
are at subagent_runtime.py:1357, untouched here). CI's lint gate is
ruff check, which is clean.

Not addressed here

_compact_bash computes stdout_budget = max(500, budget - len(stderr_keep) - overhead),
so a very large stderr can drive the stdout slice to the 500-char floor and elide
most of it even when the overall budget is generous. That is a separate question
from whether gate ② tracks gate ①, it behaves identically in both processors,
and changing it would alter output shape for the currently-working bash path —
so I left it alone rather than fold it in.

🤖 Prepared with Claude Code (Claude Opus 4.8, 1M context). Happy to split this
out or reword anything.

bash and run_python_code overflow to disk at gate ①, whose cap is
TOOL_EXEC_RESULT_MAX_CHARS (8K by default, and the knob the
tool-result-truncation A/B actually turns). Their gate ② budgets
were hardcoded here at 4K/5K, below gate ①'s own preview, so:

* run_python_code lost both the spill path and the exit status on
  every overflowing result -- at the default cap already, not only
  when it is raised;
* raising the cap gave an agent_team deployment nothing, because
  gate ② cut the extra room straight back off.

The React processor in workflows.stateful_react_agent._runtime
already tracks it (_CONFIGURED_EXEC_TOOLS + 2K footer headroom);
this wires the same rule in here, which is what the neighbouring
grep/glob 10K entries and their comment already argue for.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
D41910 added a commit to D41910/D41910 that referenced this pull request Oct 11, 2026

@zhanghanduo zhanghanduo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks for the fix.

@zhanghanduo
zhanghanduo merged commit 2251856 into ApodexAI:main Oct 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants