Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1985,6 +1985,11 @@ def __init__(
if not isinstance(_agent_section, dict):
_agent_section = {}
self._tool_use_enforcement = _agent_section.get("tool_use_enforcement", "auto")
# When tool_use_enforcement is set to the explicit string "required",
# this flag enables a runtime post-response check that nudges the
# model to actually invoke a tool when it produced a short narrative
# ack instead of a tool_call. Set in the enforcement block below.
self._tool_use_required_runtime = False

# App-level API retry count (wraps each model API call). Default 3,
# overridable via agent.api_max_retries in config.yaml. See #11616.
Expand Down Expand Up @@ -5338,6 +5343,15 @@ def _build_system_prompt(self, system_message: str = None) -> str:
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))
elif isinstance(_enforce, str) and _enforce.lower() == "required":
# "required" — inject the prompt guidance AND enable the
# runtime post-response check (see _tool_use_required_runtime).
# This goes beyond a prompt-only nudge: if the model returns a
# short narrative ack without any tool_call, the agent loop
# will append a system nudge and re-prompt instead of ending
# the turn on a promise. Compatible with any api_mode/model.
_inject = True
self._tool_use_required_runtime = True
else:
# "auto" or any unrecognised value — use hardcoded defaults
model_lower = (self.model or "").lower()
Expand Down Expand Up @@ -14351,8 +14365,16 @@ def _stop_spinner():
self._empty_content_retries = 0
self._thinking_prefill_retries = 0

# Trigger the intermediate-ack nudge when either:
# - api_mode is codex_responses (existing behaviour), OR
# - tool_use_enforcement: "required" is set (explicit
# opt-in for any model/api_mode — covers chat_completions
# paths like grok-4 via OpenRouter, DeepSeek, etc.)
if (
self.api_mode == "codex_responses"
(
self.api_mode == "codex_responses"
or getattr(self, "_tool_use_required_runtime", False)
)
and self.valid_tool_names
and codex_ack_continuations < 2
and self._looks_like_codex_intermediate_ack(
Expand Down
52 changes: 52 additions & 0 deletions tests/run_agent/test_run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,58 @@ def test_no_tools_never_injects(self):
prompt = a._build_system_prompt()
assert TOOL_USE_ENFORCEMENT_GUIDANCE not in prompt

def test_required_injects_guidance_for_any_model(self):
"""`required` injects the prompt guidance regardless of model family."""
from agent.prompt_builder import TOOL_USE_ENFORCEMENT_GUIDANCE
agent = self._make_agent(
model="anthropic/claude-sonnet-4",
tool_use_enforcement="required",
)
prompt = agent._build_system_prompt()
assert TOOL_USE_ENFORCEMENT_GUIDANCE in prompt

def test_required_sets_runtime_flag(self):
"""`required` sets the runtime flag that gates the post-response nudge."""
agent = self._make_agent(
model="x-ai/grok-4.3",
tool_use_enforcement="required",
)
# The flag is set inside _build_system_prompt() (which runs the
# enforcement block). Trigger it by building the prompt once.
agent._build_system_prompt()
assert agent._tool_use_required_runtime is True

def test_auto_does_not_set_runtime_flag(self):
"""`auto` (default) leaves the runtime flag off — only prompt-level guidance."""
agent = self._make_agent(
model="openai/gpt-4.1",
tool_use_enforcement="auto",
)
agent._build_system_prompt()
assert agent._tool_use_required_runtime is False

def test_required_is_case_insensitive(self):
"""Accept `REQUIRED`, `Required`, `required` interchangeably."""
for value in ("required", "REQUIRED", "Required"):
agent = self._make_agent(
model="x-ai/grok-4.3",
tool_use_enforcement=value,
)
agent._build_system_prompt()
assert agent._tool_use_required_runtime is True, f"failed for value={value!r}"

def test_required_runtime_flag_independent_from_api_mode(self):
"""The runtime flag is set purely from config, independent of api_mode.
This is what unlocks the post-response nudge for chat_completions models
(DeepSeek, OpenRouter grok, etc.) that previously fell through the
codex_responses-only gate."""
agent = self._make_agent(
model="deepseek/deepseek-v4-pro", # not a codex_responses model
tool_use_enforcement="required",
)
agent._build_system_prompt()
assert agent._tool_use_required_runtime is True


class TestInvalidateSystemPrompt:
def test_clears_cache(self, agent):
Expand Down
Loading