fix(agent): gate execution-guidance tool lines on the session's toolset

OPENAI_MODEL_EXECUTION_GUIDANCE told every GPT-family session to use
terminal/execute_code for arithmetic and terminal/read_file/search_files
for hashes, time, system state, files and git, even when the session's
toolset had none of those tools. Lean toolsets then refused plain
arithmetic as "prohibited mental computation".

Drop each guidance line when none of the tools it names are in
valid_tool_names. The toolset is fixed per session, so the system prompt
stays cache-stable.

Fixes #106506
This commit is contained in:
chelsealong
2026-09-09 11:46:14 +00:00
committed by Teknium
parent 105e876586
commit 41201bf215
3 changed files with 54 additions and 7 deletions

View File

@@ -506,13 +506,38 @@ OPENAI_MODEL_EXECUTION_GUIDANCE = (
)
def execution_guidance_text() -> str:
"""OPENAI_MODEL_EXECUTION_GUIDANCE as injected into the system prompt.
# Each <mandatory_tool_use>/<act_dont_ask> line above named against the tool(s) it tells the model to reach
# for. A line is dropped when none of its named tools are in the session's valid_tool_names, so a toolset
# without terminal/execute_code/etc. isn't told to use them (#106506). The
# guidance names no web tool (#39797), so there is no entry for one.
_EXECUTION_GUIDANCE_LINE_TOOLS = {
"- Arithmetic, math, calculations → use terminal or execute_code\n": {"terminal", "execute_code"},
"- Hashes, encodings, checksums → use terminal (e.g. sha256sum, base64)\n": {"terminal"},
"- Current time, date, timezone → use terminal (e.g. date)\n": {"terminal"},
"- System state: OS, CPU, memory, disk, ports, processes → use terminal\n": {"terminal"},
"- File contents, sizes, line counts → use read_file, search_files, or terminal\n": {
"read_file",
"search_files",
"terminal",
},
"- Git history, branches, diffs → use terminal\n": {"terminal"},
"- 'What time is it?' → run `date` (don't guess)\n": {"terminal"},
}
The guidance names no web tool (#39797: a hard "use web_search" overrode SOUL.md and dangled in Blank Slate),
so the text is toolset-neutral and needs no per-session filtering.
def execution_guidance_text(valid_tool_names=None) -> str:
"""OPENAI_MODEL_EXECUTION_GUIDANCE for the session's toolset (cache-safe: the toolset is fixed per session).
Lines that tell the model to reach for a tool absent from the session's toolset would dangle, so they are
dropped.
"""
return OPENAI_MODEL_EXECUTION_GUIDANCE
text = OPENAI_MODEL_EXECUTION_GUIDANCE
if valid_tool_names is not None:
valid_tool_names = set(valid_tool_names)
for line, tools in _EXECUTION_GUIDANCE_LINE_TOOLS.items():
if not tools & valid_tool_names:
text = text.replace(line, "")
return text
# Gemini/Gemma-specific operational guidance, adapted from OpenCode's gemini.txt.

View File

@@ -575,7 +575,7 @@ def _guidance_parts(agent: Any) -> List[str]:
parts.append(GOOGLE_MODEL_OPERATIONAL_GUIDANCE)
if _model_gate(getattr(agent, "_execution_guidance", "auto"), agent.model, EXECUTION_GUIDANCE_MODELS):
from agent.prompt_builder import execution_guidance_text
parts.append(execution_guidance_text())
parts.append(execution_guidance_text(agent.valid_tool_names))
# delegate_task background delivery is intentionally between turns. Put this after the generic persistence
# blocks so their "keep working" rule cannot turn the required yield into no-op/polling activity.
if "delegate_task" in agent.valid_tool_names:

View File

@@ -4,7 +4,9 @@ session can't call (Blank Slate audit, Aug 2026).
Covers:
* HERMES_AGENT_HELP_GUIDANCE degrades to the docs-only variant when the
skill tools aren't loaded.
* execution_guidance_text() never names a web tool (guidance is toolset-neutral).
* execution_guidance_text() never names a web tool, and drops the
terminal/execute_code/read_file/search_files lines the session's toolset
can't back (#106506).
* The coding operating brief drops the `todo` sentence when the todo tool
isn't loaded.
* ESSENTIAL_SKILLS can't be disabled via config, and the CLI writer strips
@@ -14,6 +16,26 @@ Covers:
from pathlib import Path
class TestExecutionGuidanceText:
def test_full_text_when_toolset_has_every_named_tool_or_is_unknown(self):
from agent.prompt_builder import OPENAI_MODEL_EXECUTION_GUIDANCE, execution_guidance_text
assert execution_guidance_text(None) == OPENAI_MODEL_EXECUTION_GUIDANCE
assert execution_guidance_text(
{"terminal", "execute_code", "read_file", "search_files"}
) == OPENAI_MODEL_EXECUTION_GUIDANCE
def test_lean_toolset_is_not_told_to_use_absent_tools(self):
# #106506: a toolset with none of terminal/execute_code/read_file/search_files must not be told to
# reach for them (GPT models then refused plain arithmetic as "prohibited mental computation").
from agent.prompt_builder import execution_guidance_text
text = execution_guidance_text({"memory"})
mandatory = text.split("<mandatory_tool_use>")[1].split("</mandatory_tool_use>")[0]
for tool in ("terminal", "execute_code", "read_file", "search_files", "Arithmetic"):
assert tool not in mandatory
assert "an appropriate permitted retrieval/search tool" in mandatory
assert "run `date`" not in text.split("<act_dont_ask>")[1].split("</act_dont_ask>")[0]
class TestCodingBriefTodoGating:
def _brief(self, valid_tool_names):
from agent.coding_context import CODING_PROFILE, RuntimeMode