feat: make tool-use enforcement configurable via agent.tool_use_enforcement (#3551)

The TOOL_USE_ENFORCEMENT_GUIDANCE injection (added in #3528) was
hardcoded to only match gpt/codex model names. This makes it a
config option so users can turn it on for any model family.

New config key: agent.tool_use_enforcement
  - "auto" (default): matches gpt/codex (existing behavior)
  - true: inject for all models
  - false: never inject
  - list of strings: custom model-name substrings to match
    e.g. ["gpt", "codex", "deepseek", "qwen"]

No version bump needed — deep merge provides the default
automatically for existing installs.

12 new tests covering all config modes.
This commit is contained in:
Teknium
2026-03-28 12:31:22 -07:00
committed by GitHub
parent d26ee20659
commit 901494d728
3 changed files with 160 additions and 7 deletions

View File

@@ -1080,6 +1080,13 @@ class AIAgent:
except Exception:
pass
# Tool-use enforcement config: "auto" (default — matches hardcoded
# model list), true (always), false (never), or list of substrings.
_agent_section = _agent_cfg.get("agent", {})
if not isinstance(_agent_section, dict):
_agent_section = {}
self._tool_use_enforcement = _agent_section.get("tool_use_enforcement", "auto")
# Initialize context compressor for automatic context management
# Compresses conversation when approaching model's context limit
# Configuration via config.yaml (compression section)
@@ -2510,14 +2517,28 @@ class AIAgent:
if tool_guidance:
prompt_parts.append(" ".join(tool_guidance))
# Some model families benefit from explicit tool-use enforcement.
# Without this, they tend to describe intended actions as text
# ("I will run the tests") instead of actually making tool calls.
# TOOL_USE_ENFORCEMENT_MODELS is a tuple of substrings to match.
# Inject only when the model has tools available.
# Tool-use enforcement: tells the model to actually call tools instead
# of describing intended actions. Controlled by config.yaml
# agent.tool_use_enforcement:
# "auto" (default) — matches TOOL_USE_ENFORCEMENT_MODELS
# true — always inject (all models)
# false — never inject
# list — custom model-name substrings to match
if self.valid_tool_names:
model_lower = (self.model or "").lower()
if any(p in model_lower for p in TOOL_USE_ENFORCEMENT_MODELS):
_enforce = self._tool_use_enforcement
_inject = False
if _enforce is True or (isinstance(_enforce, str) and _enforce.lower() in ("true", "always", "yes", "on")):
_inject = True
elif _enforce is False or (isinstance(_enforce, str) and _enforce.lower() in ("false", "never", "no", "off")):
_inject = False
elif isinstance(_enforce, list):
model_lower = (self.model or "").lower()
_inject = any(p.lower() in model_lower for p in _enforce if isinstance(p, str))
else:
# "auto" or any unrecognised value — use hardcoded defaults
model_lower = (self.model or "").lower()
_inject = any(p in model_lower for p in TOOL_USE_ENFORCEMENT_MODELS)
if _inject:
prompt_parts.append(TOOL_USE_ENFORCEMENT_GUIDANCE)
# Honcho CLI awareness: tell Hermes about its own management commands