simplify(compat): run_agent/model_tools/toolsets/acp/providers — drop 42 re-exports/aliases, repoint 15 callers + 99 test files

run_agent.py: delete the `# noqa: F401` re-export block (agent.process_bootstrap
OpenAI/_SafeWriter/_get_proxy_*, model_tools get_tool_definitions/
handle_function_call/check_toolset_requirements, FailoverReason,
_qwen_portal_headers/_routermint_headers, session_persistence names,
estimate_request_tokens_rough, ContextCompressor + friends, jittered_backoff,
prompt_builder names, message_sanitization names, tool_dispatch_helpers
names) — 41 names run_agent never used itself — and the `_STREAM_DIAG_HEADERS`
back-compat class alias (no in-tree reader). run_agent now imports only what
it uses (get_toolset_for_tool, is_local_endpoint, coalesce/uniquify tool-call
ids, cleanup_vm/get_active_env from terminal_tool_lifecycle).

agent/*: `_ra().X` late-binds that only reached a re-export now import the
defining module directly (agent_runtime_helpers -> process_bootstrap.OpenAI,
model_tools.handle_function_call, session_persistence._safe_session_filename_component;
agent_init -> model_tools.get_tool_definitions/check_toolset_requirements,
_lazy_headers("agent.client_lifecycle", ...) for qwen/routermint;
system_prompt -> agent.prompt_builder / model_tools directly, dropping its
own _ra() shim and the `_r` parameter threading). `_ra()` stays for
run_agent-resident names (logger, AIAgent, _hermes_home, _set_interrupt, ...).

toolsets.py: remove resolve_multiple_toolsets (shim-only, restored by
34abf954bd); tests/test_toolsets.py pins the same union behavior via
resolve_toolset over each name.

providers/__init__.py: drop the OMIT_TEMPERATURE re-export (no callers via the
package); ProviderProfile stays because __init__ uses it for annotations —
2 tests repointed to providers.base.

agent/iteration_budget.py: drop the "run_agent re-exports the class"
docstring pointer; 4 tests import IterationBudget from its home.

model_tools.py (arg_coercion names), agent/tool_executor.py, and
hermes_cli/cli_session_mixin.py repoints landed via a sibling commit on this
shared worktree.

Callers repointed: gateway/run.py, hermes_cli/cli_chat_turn_mixin.py,
hermes_cli/cli_tui_mixin.py, tui_gateway/session_workdir.py,
agent/transports/codex.py (one-line imports) + comment pointers in
tools/file_state.py, tools/schema_sanitizer.py, scripts/tool_search_livetest.py.
Tests: patch("run_agent.X") / monkeypatch.setattr(run_agent, "X") /
`from run_agent import X` -> defining module across 99 test files.
This commit is contained in:
Teknium
2026-09-03 13:28:22 -07:00
parent eb8a30cc2f
commit 2a95791992
114 changed files with 453 additions and 491 deletions
+8 -7
View File
@@ -1001,16 +1001,15 @@ def _lazy_headers(module: str, name: str, pass_key: bool = False, pass_base: boo
# Host → default_headers factory for explicit base_url client construction. Ordered: first
# host match wins; no match falls back to the provider profile's declared headers. ``_ra()``
# keeps the ``run_agent.*`` helpers patchable by tests.
# host match wins; no match falls back to the provider profile's declared headers.
_HOST_DEFAULT_HEADERS: List[tuple[str, Callable[[Any, str], Dict[str, str]]]] = [
("openrouter.ai", _lazy_headers("agent.auxiliary_client", "build_or_headers")),
("integrate.api.nvidia.com",
_lazy_headers("agent.auxiliary_client", "build_nvidia_nim_headers", pass_base=True)),
("api.routermint.com", lambda _k, _b: _ra()._routermint_headers()),
("api.routermint.com", _lazy_headers("agent.client_lifecycle", "_routermint_headers")),
("githubcopilot.com", _lazy_headers("hermes_cli.models", "copilot_default_headers")),
("api.kimi.com", lambda _k, _b: {"User-Agent": "claude-code/0.1.0"}),
("portal.qwen.ai", lambda _k, _b: _ra()._qwen_portal_headers()),
("portal.qwen.ai", _lazy_headers("agent.client_lifecycle", "_qwen_portal_headers")),
("chatgpt.com", _lazy_headers("agent.codex_headers", "codex_cloudflare_headers", pass_key=True)),
("x.ai", _lazy_headers("tools.xai_http", "hermes_xai_default_headers")),
]
@@ -1086,7 +1085,8 @@ def _load_tools(agent, enabled_toolsets, disabled_toolsets):
agent._tool_snapshot_generation = _snapshot_registry._generation
except Exception:
agent._tool_snapshot_generation = 0
agent.tools = _ra().get_tool_definitions(
import model_tools
agent.tools = model_tools.get_tool_definitions(
enabled_toolsets=enabled_toolsets, disabled_toolsets=disabled_toolsets,
quiet_mode=agent.quiet_mode,
)
@@ -1105,7 +1105,8 @@ def _load_tools(agent, enabled_toolsets, disabled_toolsets):
print(f" ✅ Enabled toolsets: {', '.join(enabled_toolsets)}")
if disabled_toolsets:
print(f" ❌ Disabled toolsets: {', '.join(disabled_toolsets)}")
requirements = _ra().check_toolset_requirements()
import model_tools
requirements = model_tools.check_toolset_requirements()
missing_reqs = [name for name, available in requirements.items() if not available]
if missing_reqs:
print(f"⚠️ Some tools may not work due to missing requirements: {missing_reqs}")
@@ -1970,7 +1971,7 @@ def _inject_context_engine_tools(agent):
# Context engine tool schemas (lcm_*), deduped against existing names (plugins may
# register the same schemas; duplicates 400 provider-side) and gated on enabled_toolsets
# so `platform_toolsets: telegram: []` can't leak them.
# Skip names that are already present — the _ra().get_tool_definitions() quiet_mode cache returned a
# Skip names that are already present — the model_tools.get_tool_definitions() quiet_mode cache returned a
# shared list pre-#17335, so a stray mutation here would poison subsequent agent inits in the same
# Gateway process and trip provider-side 'duplicate tool name' errors. Even with the cache fix, dedup is
# the right defense against plugin paths that may register the same schemas via ctx.register_tool().
+7 -4
View File
@@ -1278,7 +1278,8 @@ def dump_api_request_debug(
dump_payload["error"] = _api_error_debug_info(error)
# Sanitize the session ID (may come from an untrusted X-Hermes-Session-Id header) so a
# "../"-shaped ID cannot write outside logs_dir.
safe_sid = _ra()._safe_session_filename_component(agent.session_id)
from agent.session_persistence import _safe_session_filename_component
safe_sid = _safe_session_filename_component(agent.session_id)
dump_file = agent.logs_dir / f"request_dump_{safe_sid}_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.json"
# Redact secrets first: this fires unconditionally on API errors and captures the full
# request body, so context-embedded secrets would otherwise land in cleartext on disk.
@@ -1750,8 +1751,9 @@ def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: boo
client_kwargs, access_token=client_kwargs.get("api_key", ""),
base_url=str(client_kwargs.get("base_url", "")),
)
# Module-level `OpenAI` is resolved lazily via __getattr__; tests patch `run_agent.OpenAI`.
client = _ra().OpenAI(**client_kwargs)
# ``process_bootstrap.OpenAI`` is a lazy SDK proxy; resolved at call time so tests can patch it.
from agent import process_bootstrap
client = process_bootstrap.OpenAI(**client_kwargs)
_ra().logger.info("OpenAI client created (%s, shared=%s) %s", reason, shared, agent._client_log_context())
return client
@@ -2247,7 +2249,8 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
)
if skip_tool_execution_middleware:
dispatch_kwargs["skip_tool_execution_middleware"] = True
return _ra().handle_function_call(function_name, next_args, effective_task_id, **dispatch_kwargs)
import model_tools
return model_tools.handle_function_call(function_name, next_args, effective_task_id, **dispatch_kwargs)
if skip_tool_execution_middleware:
return _execute(function_args)
from hermes_cli.middleware import run_tool_execution_middleware
-1
View File
@@ -3,7 +3,6 @@
Each ``AIAgent`` (parent or subagent) holds its own :class:`IterationBudget`: the parent's
cap is ``max_iterations`` (default 500), each subagent's ``delegation.max_iterations``
(default 50), so total iterations across parent + subagents can exceed the parent's cap.
``run_agent`` re-exports the class so ``from run_agent import IterationBudget`` keeps working.
"""
from __future__ import annotations
+4 -4
View File
@@ -1,9 +1,9 @@
"""Process-level bootstrap helpers for ``run_agent``.
Lazy OpenAI SDK import (``_OpenAIProxy`` keeps ``isinstance`` and
``patch("run_agent.OpenAI")`` working), crash-resistant stdio (``_SafeWriter``),
env-only HTTP proxy resolution, and Codex dual-stack (Happy Eyeballs)
connection racing. ``run_agent`` re-exports every name.
``patch("agent.process_bootstrap.OpenAI")`` working), crash-resistant stdio
(``_SafeWriter``), env-only HTTP proxy resolution, and Codex dual-stack
(Happy Eyeballs) connection racing.
"""
from __future__ import annotations
@@ -451,7 +451,7 @@ def _install_safe_stdio() -> None:
setattr(sys, stream_name, _SafeWriter(stream))
# Drop-in for ``openai.OpenAI`` (also re-exported via ``run_agent``).
# Drop-in for ``openai.OpenAI``.
OpenAI = _OpenAIProxy()
+10 -16
View File
@@ -24,6 +24,7 @@ from agent.prompt_builder import (
SKILLS_GUIDANCE, STEER_CHANNEL_NOTE, TASK_COMPLETION_GUIDANCE, TELEGRAM_RICH_MESSAGES_HINT,
TOOL_USE_ENFORCEMENT_GUIDANCE, TOOL_USE_ENFORCEMENT_MODELS, drain_truncation_warnings,
)
from agent import prompt_builder as _pb
from agent.runtime_cwd import resolve_context_cwd
from hermes_constants import get_default_hermes_root, get_hermes_home
from utils import is_truthy_value
@@ -36,13 +37,6 @@ _PLUGIN_SECTION_FRAME_RE = re.compile(
_GATE_WORDS = {**dict.fromkeys(("true", "always", "yes", "on"), True), **dict.fromkeys(("false", "never", "no", "off"), False)}
def _ra():
"""Lazy ``run_agent`` handle: tests ``patch("run_agent.load_soul_md")`` etc.,
so the helpers must be resolved through that namespace on every call."""
import run_agent
return run_agent
def _model_gate(setting: Any, model: Optional[str], default_models) -> bool:
"""Resolve a config gate: True/"true"-ish -> on, False/"false"-ish -> off,
list -> case-insensitive model-substring match, anything else ("auto") ->
@@ -301,18 +295,19 @@ def _tool_guidance_block(agent: Any) -> Optional[str]:
return " ".join(g for g in tool_guidance if g) or None
def _skills_prompt(agent: Any, _r: Any) -> str:
def _skills_prompt(agent: Any) -> str:
"""Skills index (empty without skills tools). Focus mode demotes non-coding
categories to names-only — never hidden, every name stays visible."""
if not any(name in agent.valid_tool_names for name in ['skills_list', 'skill_view', 'skill_manage']):
return ""
avail_toolsets = {_r.get_toolset_for_tool(tool_name) for tool_name in agent.valid_tool_names} - {None, ""}
import model_tools
avail_toolsets = {model_tools.get_toolset_for_tool(tool_name) for tool_name in agent.valid_tool_names} - {None, ""}
try:
from agent.coding_context import coding_compact_skill_categories
_compact_cats = coding_compact_skill_categories(platform=agent.platform, cwd=resolve_context_cwd())
except Exception:
_compact_cats = frozenset()
return _r.build_skills_system_prompt(available_tools=agent.valid_tool_names, available_toolsets=avail_toolsets,
return _pb.build_skills_system_prompt(available_tools=agent.valid_tool_names, available_toolsets=avail_toolsets,
compact_categories=_compact_cats or None, skills_dir_override=_agent_skills_dir(agent))
@@ -489,12 +484,12 @@ def _memory_parts(agent: Any) -> List[str]:
return parts
def _identity_parts(agent: Any, _r: Any, ctx_len: Optional[int]) -> Tuple[List[str], bool]:
def _identity_parts(agent: Any, ctx_len: Optional[int]) -> Tuple[List[str], bool]:
"""SOUL.md (primary identity; cron keeps the persona while skipping cwd
instructions, scoped to the agent's OWN home) or the default identity.
Returns ``(parts, soul_loaded)``."""
wants_soul = agent.load_soul_identity or not agent.skip_context_files
_soul_content = _r.load_soul_md(ctx_len, home_override=_agent_home(agent)) if wants_soul else None
_soul_content = _pb.load_soul_md(ctx_len, home_override=_agent_home(agent)) if wants_soul else None
return ([_soul_content], True) if _soul_content else ([DEFAULT_AGENT_IDENTITY], False)
@@ -571,7 +566,7 @@ def _post_workspace_parts(agent: Any) -> List[str]:
return parts
def _context_files_part(agent: Any, _r: Any, ctx_len: Optional[int], soul_loaded: bool) -> List[str]:
def _context_files_part(agent: Any, ctx_len: Optional[int], soul_loaded: bool) -> List[str]:
"""Project context files (AGENTS.md etc.) for the context tier. TERMINAL_CWD
when set (gateway); None lets discovery fall back to the launch dir. The
install-tree fallback is only legitimate for cli/tui where the launch dir
@@ -580,7 +575,7 @@ def _context_files_part(agent: Any, _r: Any, ctx_len: Optional[int], soul_loaded
if agent.skip_context_files:
return []
launch_artifact = getattr(agent, "_context_cwd_is_launch_artifact", False)
return [_r.build_context_files_prompt(
return [_pb.build_context_files_prompt(
cwd=None if launch_artifact else resolve_context_cwd(), skip_soul=soul_loaded, context_length=ctx_len,
allow_install_tree_fallback=agent.platform in ("cli", "tui"), home_override=_agent_home(agent))]
@@ -596,7 +591,6 @@ def build_system_prompt_parts(agent: Any, system_message: Optional[str] = None)
(snapshot, remaining session-stable guidance, caller ``system_message``,
context files) and ``volatile`` (skills index, memory, user profile, external
memory block, timestamp line). Never re-rendered mid-session."""
_r = _ra()
# Model context window scales the context-file caps; stable per conversation.
_cc_len = getattr(getattr(agent, "context_compressor", None), "context_length", None)
_ctx_len = _cc_len if isinstance(_cc_len, int) and _cc_len > 0 else None
@@ -614,7 +608,7 @@ def build_system_prompt_parts(agent: Any, system_message: Optional[str] = None)
if "skill_view" in (agent.valid_tool_names or set()) and "- hermes-agent:" in skills_prompt:
stable_parts[_help_guidance_slot] = HERMES_AGENT_HELP_GUIDANCE
stable_parts.extend(_alibaba_identity_part(agent))
stable_parts.append(_r.build_environment_hints())
stable_parts.append(_pb.build_environment_hints())
# Coding posture: operating brief stays in the stable prefix; the live
# git/workspace snapshot sits behind its own cache boundary, and the blocks
# below it must keep their historical post-snapshot position.
+1 -1
View File
@@ -479,7 +479,7 @@ class ResponsesApiTransport(ProviderTransport):
is_codex_backend: bool — chatgpt.com/backend-api/codex is_xai_responses: bool — xAI/Grok backend
github_reasoning_extra: dict | None — Copilot reasoning params
"""
from run_agent import DEFAULT_AGENT_IDENTITY
from agent.prompt_builder import DEFAULT_AGENT_IDENTITY
instructions = params.get("instructions", "")
payload_messages = messages
+2 -2
View File
@@ -1218,7 +1218,7 @@ def _select_cached_agent_history(
retention.
"""
if isinstance(live_history, list) and len(live_history) > len(persisted_history):
from run_agent import _is_ephemeral_scaffolding
from agent.session_persistence import _is_ephemeral_scaffolding
has_unpersisted_row = any(
isinstance(message, dict) and not message.get("_db_persisted")
@@ -2916,7 +2916,7 @@ def _format_gateway_process_notification(evt: dict) -> "str | None":
return text
if evt_type == "async_delegation":
from tools.process_registry import format_process_notification
from tools.process_registry_notifications import format_process_notification
return format_process_notification(evt)
return None
+1 -1
View File
@@ -61,7 +61,7 @@ class CLIChatTurnMixin:
if blocked is not None:
return blocked
# Lone surrogates (rich-text clipboard paste) crash the OpenAI SDK's JSON serialization.
from run_agent import _sanitize_surrogates
from agent.message_sanitization import _sanitize_surrogates
message = _sanitize_surrogates(message)
self._chat_stage_user_message(agent, message)
+1 -1
View File
@@ -1610,7 +1610,7 @@ class CLITuiMixin:
event.app.invalidate()
if pasted_text:
# Sanitize surrogates (Word/Google Docs paste) before writing.
from run_agent import _sanitize_surrogates
from agent.message_sanitization import _sanitize_surrogates
pasted_text = _sanitize_surrogates(pasted_text)
line_count = pasted_text.count('\n')
buf = event.current_buffer
+1 -1
View File
@@ -38,7 +38,7 @@ import logging
import sys
from pathlib import Path
from providers.base import OMIT_TEMPERATURE, ProviderProfile # noqa: F401
from providers.base import ProviderProfile
logger = logging.getLogger(__name__)
+6 -30
View File
@@ -87,10 +87,6 @@ def _gateway_origin_json(agent: "AIAgent") -> Optional[str]:
return None
# Every F401-suppressed import below is a re-export reached via `patch("run_agent.<X>")`,
# `from run_agent import X`, or `_ra().<X>` from the agent/ helpers — keep them resolving here.
# `OpenAI` is a lazy proxy (SDK import costs ~240ms) that keeps `patch("run_agent.OpenAI")` working.
from agent.process_bootstrap import OpenAI, _SafeWriter, _get_proxy_for_base_url, _get_proxy_from_env # noqa: F401
from agent.iteration_budget import IterationBudget
from hermes_cli.env_loader import load_hermes_dotenv
from hermes_cli.timeouts import get_provider_request_timeout, get_provider_stale_timeout
@@ -103,14 +99,13 @@ if not _loaded_env_paths:
logger.info("No .env file found. Using system environment variables.")
from model_tools import get_tool_definitions, get_toolset_for_tool, handle_function_call, check_toolset_requirements # noqa: F401
from model_tools import get_toolset_for_tool
from tools.terminal_tool_lifecycle import cleanup_vm, get_active_env
from tools.interrupt import set_interrupt as _set_interrupt
from tools.browser_tool import cleanup_browser
from agent.memory_provider import is_trivial_prompt
from agent.error_classifier import FailoverReason # noqa: F401
from agent.client_lifecycle import ClientLifecycleMixin, _qwen_portal_headers, _routermint_headers # noqa: F401
from agent.client_lifecycle import ClientLifecycleMixin
from agent.stream_delivery import StreamDeliveryMixin
from agent.status_output import StatusOutputMixin
from agent.api_request_hooks import ApiRequestHooksMixin
@@ -119,28 +114,15 @@ from agent.interrupt_control import InterruptControlMixin
from agent.turn_explainers import TurnExplainersMixin
from agent.activity_tracking import ActivityTrackingMixin
from agent.rate_limit_credits import RateLimitCreditsMixin
from agent.session_persistence import ( # noqa: F401
SessionPersistenceMixin, _DB_PERSISTED_MARKER, _EPHEMERAL_SCAFFOLDING_FLAGS,
_is_ephemeral_scaffolding, _safe_session_filename_component,
)
from agent.session_persistence import SessionPersistenceMixin
from agent.compression_facade import CompressionFacadeMixin
from agent.turn_facade import TurnFacadeMixin
from agent.vision_message_prep import VisionMessagePrepMixin
from agent.reasoning_params import ReasoningParamsMixin
from agent.lazy_forward import forward as _forward, forward_static as _forward_static
from agent.session_activity import ActivityProvenance
from agent.model_metadata import estimate_request_tokens_rough, is_local_endpoint # noqa: F401
from agent.context_compressor import COMPRESSED_SUMMARY_METADATA_KEY, ContextCompressor, user_originated_turn_view # noqa: F401
from agent.retry_utils import jittered_backoff # noqa: F401
from agent.prompt_builder import ( # noqa: F401
DEFAULT_AGENT_IDENTITY, build_skills_system_prompt, build_context_files_prompt,
build_environment_hints, load_soul_md,
)
from agent.message_sanitization import ( # noqa: F401
_SURROGATE_RE, _sanitize_surrogates, _sanitize_structure_surrogates, _sanitize_messages_surrogates,
_escape_invalid_chars_in_json_strings, _repair_tool_call_arguments, _strip_non_ascii,
_sanitize_messages_non_ascii, _sanitize_tools_non_ascii, _looks_like_image_content_rejection,
_strip_images_from_messages, _sanitize_structure_non_ascii,
from agent.model_metadata import is_local_endpoint
from agent.message_sanitization import (
coalesce_tool_call_id as _sanitize_coalesce_tool_call_id,
uniquify_tool_call_ids as _sanitize_uniquify_tool_call_ids,
)
@@ -151,10 +133,6 @@ from agent.codex_responses_adapter import (
_summarize_user_message_for_log,
)
from agent.tool_guardrails import ToolGuardrailDecision, append_toolguard_guidance, toolguard_synthetic_result
from agent.tool_dispatch_helpers import ( # noqa: F401
_should_parallelize_tool_batch, _is_destructive_command, _extract_parallel_scope_path, _paths_overlap,
_append_subdir_hint_to_multimodal, _trajectory_normalize_msg,
)
from utils import base_url_host_matches, base_url_hostname, env_float, model_forces_max_completion_tokens
@@ -493,8 +471,6 @@ class AIAgent(
self._codex_reasoning_replay_enabled = False
return {"messages": stripped_messages, "items": stripped_items}
# Backward-compat alias; the list lives in ``agent.stream_diag.STREAM_DIAG_HEADERS``.
from agent.stream_diag import STREAM_DIAG_HEADERS as _STREAM_DIAG_HEADERS # noqa: E402
_stream_diag_init = _forward_static("agent.stream_diag", "stream_diag_init")
_stream_diag_capture_response = _forward("agent.stream_diag", "stream_diag_capture_response")
_flatten_exception_chain = _forward_static("agent.stream_diag", "flatten_exception_chain")
@@ -955,7 +931,7 @@ class AIAgent(
process_registry.kill_all(task_id=task_id)
def release_computer_use() -> None:
from tools.computer_use.tool import release_computer_use_session
from tools.computer_use import release_computer_use_session
release_computer_use_session(task_id)
for step in (kill_processes, lambda: cleanup_vm(task_id), lambda: cleanup_browser(task_id), release_computer_use):
+1 -1
View File
@@ -366,7 +366,7 @@ def run_one_scenario(scenario: Dict[str, Any], enabled: bool, out_dir: Path) ->
n_registered = register_fake_tools()
# Capture tool calls via a hook on the registry dispatch path. We use the
# registry hook (rather than the run_agent.handle_function_call binding,
# registry hook (rather than the model_tools.handle_function_call binding,
# which is already cached by tool_executor) because the dispatch call is
# the one place every underlying tool call lands. Bridge calls are
# extracted from the message transcript after the run.
+2 -2
View File
@@ -51,8 +51,8 @@ def _agent(monkeypatch, base_url: str, **kwargs):
from run_agent import AIAgent
client = _FakeClient()
monkeypatch.setattr("run_agent.OpenAI", lambda **_kw: client)
monkeypatch.setattr("run_agent.get_tool_definitions", lambda *a, **k: [])
monkeypatch.setattr("agent.process_bootstrap.OpenAI", lambda **_kw: client)
monkeypatch.setattr("model_tools.get_tool_definitions", lambda *a, **k: [])
agent = AIAgent(
model="gpt-5", # a model that would normally trigger the Responses upgrade
api_key="test-key",
+2 -2
View File
@@ -99,7 +99,7 @@ class TestPrimaryClientWiring:
"""Credential-rotation / base-url change path must also emit codex headers."""
from run_agent import AIAgent
token = _make_codex_jwt("acct-rotation")
with patch("run_agent.OpenAI") as mock_openai:
with patch("agent.process_bootstrap.OpenAI") as mock_openai:
mock_openai.return_value = MagicMock()
agent = AIAgent(
api_key="placeholder-openrouter-key",
@@ -124,7 +124,7 @@ class TestPrimaryClientWiring:
"""Switching AWAY from chatgpt.com must drop the codex headers."""
from run_agent import AIAgent
token = _make_codex_jwt()
with patch("run_agent.OpenAI") as mock_openai:
with patch("agent.process_bootstrap.OpenAI") as mock_openai:
mock_openai.return_value = MagicMock()
agent = AIAgent(
api_key=token,
@@ -20,7 +20,8 @@ from __future__ import annotations
from pathlib import Path
from types import SimpleNamespace
from hermes_state import SessionDB, _DB_PERSISTED_MARKER_KEY
from hermes_state import SessionDB
from agent.context_compressor import _DB_PERSISTED_MARKER as _DB_PERSISTED_MARKER_KEY
from run_agent import AIAgent
@@ -68,9 +69,9 @@ def test_marker_constant_in_sync() -> None:
"""
import agent.context_compressor as cc
import agent.turn_finalizer as tf
import run_agent
import agent.session_persistence as sp
assert _DB_PERSISTED_MARKER_KEY == run_agent._DB_PERSISTED_MARKER
assert _DB_PERSISTED_MARKER_KEY == sp._DB_PERSISTED_MARKER
assert _DB_PERSISTED_MARKER_KEY == cc._DB_PERSISTED_MARKER
assert _DB_PERSISTED_MARKER_KEY == tf._DB_PERSISTED_MARKER
+1 -1
View File
@@ -1262,7 +1262,7 @@ class TestStripProviderPrefix:
def test_registered_profile_name_and_alias_are_stripped(self, monkeypatch):
import providers
from providers import ProviderProfile
from providers.base import ProviderProfile
monkeypatch.setattr(providers, "_REGISTRY", {})
monkeypatch.setattr(providers, "_ALIASES", {})
+3 -3
View File
@@ -26,9 +26,9 @@ from agent.system_prompt import (
def _stable_prompt(agent):
with (
patch("run_agent.load_soul_md", return_value=""),
patch("run_agent.build_environment_hints", return_value=""),
patch("run_agent.build_context_files_prompt", return_value=""),
patch("agent.prompt_builder.load_soul_md", return_value=""),
patch("agent.prompt_builder.build_environment_hints", return_value=""),
patch("agent.prompt_builder.build_context_files_prompt", return_value=""),
):
return build_system_prompt_parts(agent)["stable"]
@@ -133,7 +133,7 @@ def test_full_prompt_scoped_to_bot_on_bare_thread(tmp_path, monkeypatch):
def build():
with (
patch("run_agent.build_environment_hints", return_value=""),
patch("agent.prompt_builder.build_environment_hints", return_value=""),
):
result["prompt"] = build_system_prompt(agent)
+2 -2
View File
@@ -157,10 +157,10 @@ def _run_turn(monkeypatch, *, projected, iterations):
from run_agent import AIAgent
monkeypatch.setattr(
"run_agent.OpenAI",
"agent.process_bootstrap.OpenAI",
lambda **_kw: _FakeAgentProviderClient(projected, iterations),
)
monkeypatch.setattr("run_agent.get_tool_definitions", lambda *a, **k: [])
monkeypatch.setattr("model_tools.get_tool_definitions", lambda *a, **k: [])
agent = AIAgent(
model="test-model",
+1 -1
View File
@@ -33,7 +33,7 @@ class _StubClient:
def _make_agent(provider="openai", base_url="https://api.openai.com/v1", model="gpt-5.4"):
with patch("run_agent.OpenAI") as mock_openai:
with patch("agent.process_bootstrap.OpenAI") as mock_openai:
mock_openai.return_value = MagicMock()
agent = AIAgent(
api_key="sk-test",
+3 -3
View File
@@ -27,9 +27,9 @@ class _FakeOpenAI:
def _make_agent(
monkeypatch, enabled_toolsets=None, disabled_toolsets=None, skip_memory=True
):
monkeypatch.setattr("run_agent.get_tool_definitions", lambda **kw: [])
monkeypatch.setattr("run_agent.check_toolset_requirements", lambda: {})
monkeypatch.setattr("run_agent.OpenAI", _FakeOpenAI)
monkeypatch.setattr("model_tools.get_tool_definitions", lambda **kw: [])
monkeypatch.setattr("model_tools.check_toolset_requirements", lambda: {})
monkeypatch.setattr("agent.process_bootstrap.OpenAI", _FakeOpenAI)
return AIAgent(
api_key="test-key",
base_url="http://test",
+3 -3
View File
@@ -83,9 +83,9 @@ def db(tmp_path):
def _bridge_agent(session_db, session_id: str, *, declared_key: str | None):
"""Construct an agent the way the Studio bridge pool does."""
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key",
+27 -27
View File
@@ -47,9 +47,9 @@ def _captured_context_cwd(agent):
return ""
with (
patch("run_agent.load_soul_md", return_value=""),
patch("run_agent.build_environment_hints", return_value=""),
patch("run_agent.build_context_files_prompt", side_effect=fake_context_files),
patch("agent.prompt_builder.load_soul_md", return_value=""),
patch("agent.prompt_builder.build_environment_hints", return_value=""),
patch("agent.prompt_builder.build_context_files_prompt", side_effect=fake_context_files),
):
build_system_prompt_parts(agent)
return captured["cwd"]
@@ -80,8 +80,8 @@ class TestContextFileCwd:
_context_cwd_is_launch_artifact=True,
)
with (
patch("run_agent.load_soul_md", return_value=""),
patch("run_agent.build_environment_hints", return_value=""),
patch("agent.prompt_builder.load_soul_md", return_value=""),
patch("agent.prompt_builder.build_environment_hints", return_value=""),
patch("agent.system_prompt.resolve_context_cwd", return_value=tmp_path),
):
context = build_system_prompt_parts(agent)["context"]
@@ -102,8 +102,8 @@ class TestContextFileCwd:
_context_cwd_is_launch_artifact=False,
)
with (
patch("run_agent.load_soul_md", return_value=""),
patch("run_agent.build_environment_hints", return_value=""),
patch("agent.prompt_builder.load_soul_md", return_value=""),
patch("agent.prompt_builder.build_environment_hints", return_value=""),
patch("agent.system_prompt.resolve_context_cwd", return_value=tmp_path),
):
context = build_system_prompt_parts(agent)["context"]
@@ -113,18 +113,18 @@ class TestContextFileCwd:
def _stable_prompt(agent):
with (
patch("run_agent.load_soul_md", return_value=""),
patch("run_agent.build_environment_hints", return_value=""),
patch("run_agent.build_context_files_prompt", return_value=""),
patch("agent.prompt_builder.load_soul_md", return_value=""),
patch("agent.prompt_builder.build_environment_hints", return_value=""),
patch("agent.prompt_builder.build_context_files_prompt", return_value=""),
):
return build_system_prompt_parts(agent)["stable"]
def _prompt_parts(agent):
with (
patch("run_agent.load_soul_md", return_value=""),
patch("run_agent.build_environment_hints", return_value=""),
patch("run_agent.build_context_files_prompt", return_value=""),
patch("agent.prompt_builder.load_soul_md", return_value=""),
patch("agent.prompt_builder.build_environment_hints", return_value=""),
patch("agent.prompt_builder.build_context_files_prompt", return_value=""),
):
return build_system_prompt_parts(agent)
@@ -311,9 +311,9 @@ class TestNamedProfileHintIntegration:
def test_build_system_prompt_records_stable_prefix():
agent = _make_agent()
with (
patch("run_agent.load_soul_md", return_value=""),
patch("run_agent.build_environment_hints", return_value=""),
patch("run_agent.build_context_files_prompt", return_value="context"),
patch("agent.prompt_builder.load_soul_md", return_value=""),
patch("agent.prompt_builder.build_environment_hints", return_value=""),
patch("agent.prompt_builder.build_context_files_prompt", return_value="context"),
):
prompt = build_system_prompt(agent)
@@ -360,9 +360,9 @@ def test_coding_prompt_preserves_legacy_workspace_order(monkeypatch):
))
with (
patch("run_agent.load_soul_md", return_value=""),
patch("run_agent.build_environment_hints", return_value=""),
patch("run_agent.build_context_files_prompt", return_value="CONTEXT_FILES"),
patch("agent.prompt_builder.load_soul_md", return_value=""),
patch("agent.prompt_builder.build_environment_hints", return_value=""),
patch("agent.prompt_builder.build_context_files_prompt", return_value="CONTEXT_FILES"),
patch(
"agent.coding_context.coding_system_prompt_parts",
return_value=(
@@ -504,11 +504,11 @@ def _build(builder, **overrides):
"""Run a build_* function with skills + context files present."""
agent = _make_agent(valid_tool_names=["skills_list"], **overrides)
with (
patch("run_agent.load_soul_md", return_value=""),
patch("run_agent.build_environment_hints", return_value=""),
patch("run_agent.build_context_files_prompt", return_value=_CONTEXT),
patch("run_agent.get_toolset_for_tool", return_value=None),
patch("run_agent.build_skills_system_prompt", return_value=_SKILLS),
patch("agent.prompt_builder.load_soul_md", return_value=""),
patch("agent.prompt_builder.build_environment_hints", return_value=""),
patch("agent.prompt_builder.build_context_files_prompt", return_value=_CONTEXT),
patch("model_tools.get_toolset_for_tool", return_value=None),
patch("agent.prompt_builder.build_skills_system_prompt", return_value=_SKILLS),
):
return builder(agent)
@@ -710,9 +710,9 @@ def test_conversation_start_uses_session_start_not_build_time(monkeypatch):
monkeypatch.setattr(system_prompt, "get_hermes_home", lambda: Path("/hermes"))
with (
patch("run_agent.load_soul_md", return_value=""),
patch("run_agent.build_environment_hints", return_value=""),
patch("run_agent.build_context_files_prompt", return_value="CONTEXT_FILES"),
patch("agent.prompt_builder.load_soul_md", return_value=""),
patch("agent.prompt_builder.build_environment_hints", return_value=""),
patch("agent.prompt_builder.build_context_files_prompt", return_value="CONTEXT_FILES"),
patch(
"agent.coding_context.coding_system_prompt_parts",
return_value=([], [], []),
+1 -1
View File
@@ -96,7 +96,7 @@ class TestCLISubagentInterrupt(unittest.TestCase):
# Patch AIAgent to use our mock
from tools.delegate_tool import _run_single_child
from run_agent import IterationBudget
from agent.iteration_budget import IterationBudget
parent.iteration_budget = IterationBudget(max_total=100)
+4 -4
View File
@@ -241,10 +241,10 @@ def _make_agent(tool_progress_mode: str):
}
]
with (
patch("run_agent.get_tool_definitions", return_value=tool_defs),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("model_tools.get_tool_definitions", return_value=tool_defs),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("hermes_cli.config.load_config", return_value={}),
patch("run_agent.OpenAI"),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key-1234567890",
@@ -292,7 +292,7 @@ def _run_fake_turn(tool_progress_mode: str, dispatch_mode: str = "sequential"):
return json.dumps({"ok": args["query"]})
with (
patch("run_agent.handle_function_call", side_effect=fake_dispatch),
patch("model_tools.handle_function_call", side_effect=fake_dispatch),
patch.object(agent, "_invoke_tool", side_effect=fake_dispatch),
patch(
"agent.tool_executor.maybe_persist_tool_result",
+4 -5
View File
@@ -17,8 +17,7 @@ from gateway.session import SessionSource
def _patch_agent_bootstrap(monkeypatch):
monkeypatch.setattr(
run_agent,
"get_tool_definitions",
"model_tools.get_tool_definitions",
lambda **kwargs: [
{
"type": "function",
@@ -30,7 +29,7 @@ def _patch_agent_bootstrap(monkeypatch):
}
],
)
monkeypatch.setattr(run_agent, "check_toolset_requirements", lambda: {})
monkeypatch.setattr("model_tools.check_toolset_requirements", lambda: {})
def _codex_message_response(text: str):
@@ -94,7 +93,7 @@ class _Codex401ThenSuccessAgent(run_agent.AIAgent):
def test_cron_run_job_codex_path_handles_internal_401_refresh(monkeypatch):
_patch_agent_bootstrap(monkeypatch)
monkeypatch.setattr(run_agent, "OpenAI", _FakeOpenAI)
monkeypatch.setattr("agent.process_bootstrap.OpenAI", _FakeOpenAI)
monkeypatch.setattr(run_agent, "AIAgent", _Codex401ThenSuccessAgent)
monkeypatch.setattr(
"hermes_cli.runtime_provider.resolve_runtime_provider",
@@ -125,7 +124,7 @@ def test_cron_run_job_codex_path_handles_internal_401_refresh(monkeypatch):
def test_gateway_run_agent_codex_path_handles_internal_401_refresh(monkeypatch):
_patch_agent_bootstrap(monkeypatch)
monkeypatch.setattr(run_agent, "OpenAI", _FakeOpenAI)
monkeypatch.setattr("agent.process_bootstrap.OpenAI", _FakeOpenAI)
monkeypatch.setattr(run_agent, "AIAgent", _Codex401ThenSuccessAgent)
monkeypatch.setattr(
gateway_run,
@@ -248,7 +248,7 @@ def test_e2e_cron_turn_is_bounded_through_the_real_agent_routing(monkeypatch):
wire = _StallingWireClient()
agent = _build_cron_agent(monkeypatch)
agent.client = wire
monkeypatch.setattr(run_agent, "OpenAI", lambda **_kwargs: wire)
monkeypatch.setattr("agent.process_bootstrap.OpenAI", lambda **_kwargs: wire)
monkeypatch.setattr(
run_agent.AIAgent,
"_force_close_tcp_sockets",
@@ -250,7 +250,7 @@ class TestOllamaCloudAgentInit:
def test_ollama_cloud_agent_uses_chat_completions(self, monkeypatch):
"""Ollama Cloud falls through to chat_completions — no special elif needed."""
monkeypatch.setenv("OLLAMA_API_KEY", "test-key")
with patch("run_agent.OpenAI") as mock_openai:
with patch("agent.process_bootstrap.OpenAI") as mock_openai:
mock_openai.return_value = MagicMock()
from run_agent import AIAgent
agent = AIAgent(
+1 -1
View File
@@ -1,6 +1,6 @@
import pytest
from providers import ProviderProfile
from providers.base import ProviderProfile
import providers
@@ -23,9 +23,9 @@ class TestGeneric400Heuristic:
def _make_agent(self):
"""Create a minimal AIAgent for testing error handling."""
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
from run_agent import AIAgent
a = AIAgent(
@@ -23,9 +23,9 @@ from agent.chat_completion_helpers import _FALLBACK_EXHAUSTED_COOLDOWN_S
def _make_agent(fallback_model=None):
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key",
@@ -46,9 +46,9 @@ def _make_tool_defs():
def _make_agent_with_fallback(fb_chain):
"""Build a minimal AIAgent with the given fallback chain configured."""
with (
patch("run_agent.get_tool_definitions", return_value=_make_tool_defs()),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI", return_value=MagicMock()),
patch("model_tools.get_tool_definitions", return_value=_make_tool_defs()),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()),
):
agent = AIAgent(
api_key="primary-key-abcdef12",
@@ -264,7 +264,7 @@ class TestFallbackChainResetOnTransportRecovery:
patch.object(agent, "_persist_session"),
patch.object(agent, "_save_trajectory"),
patch.object(agent, "_cleanup_task_resources"),
patch("run_agent.OpenAI", return_value=MagicMock()),
patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()),
patch("agent.agent_runtime_helpers.time.sleep"),
patch(
"agent.auxiliary_client.resolve_provider_client",
@@ -36,7 +36,7 @@ class TestCredentialPoolPreservedOnAutoDetect:
pool = SimpleNamespace(provider="anthropic")
with patch("agent.auxiliary_client.resolve_provider_client", return_value=(None, None)), \
patch("run_agent.get_tool_definitions", return_value=[]), \
patch("model_tools.get_tool_definitions", return_value=[]), \
patch('agent.anthropic_adapter.build_anthropic_client', return_value=MagicMock()), \
patch('agent.anthropic_credentials.resolve_anthropic_token', return_value=''), \
patch('agent.anthropic_credentials._is_oauth_token', return_value=False), \
@@ -218,9 +218,9 @@ def _make_agent():
from run_agent import AIAgent
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI", return_value=MagicMock()),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()),
):
agent = AIAgent(
api_key="fx", # unused — the OpenAI client is mocked below
@@ -291,7 +291,7 @@ class TestRunConversationRecoversFromCorruptImage400:
patch.object(agent, "_persist_session"),
patch.object(agent, "_save_trajectory"),
patch.object(agent, "_cleanup_task_resources"),
patch("run_agent.OpenAI", return_value=MagicMock()),
patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()),
patch("agent.agent_runtime_helpers.time.sleep"),
patch("agent.model_metadata.get_model_context_length", return_value=200000),
):
@@ -382,7 +382,7 @@ class TestRunConversationRecoversFromCorruptImage400:
patch.object(agent, "_persist_session"),
patch.object(agent, "_save_trajectory"),
patch.object(agent, "_cleanup_task_resources"),
patch("run_agent.OpenAI", return_value=MagicMock()),
patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()),
patch("agent.agent_runtime_helpers.time.sleep"),
patch("agent.model_metadata.get_model_context_length", return_value=200000),
):
@@ -218,7 +218,7 @@ class TestReplacePrimaryRetiresInsteadOfClosing:
agent, "_force_close_tcp_sockets",
side_effect=lambda c: shutdown_calls.append(c) or 1,
):
with patch("run_agent.OpenAI", MagicMock()):
with patch("agent.process_bootstrap.OpenAI", MagicMock()):
ok = agent._replace_primary_openai_client(reason="test_rotate")
assert ok
@@ -39,9 +39,9 @@ def loop_agent():
"""AIAgent with a mocked OpenAI client (mirrors test_run_agent's fixture)."""
from run_agent import AIAgent
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key-1234567890",
@@ -42,9 +42,9 @@ def loop_agent():
from tests.run_agent.test_run_agent import _mock_response
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key-1234567890",
@@ -34,9 +34,9 @@ _API_KEY_TOKEN = "sk-ant-api-abcdef1234567890"
def agent():
"""Minimal AIAgent construction, skipping tool discovery."""
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
a = AIAgent(
api_key="test-key-1234567890",
@@ -105,8 +105,8 @@ class TestOAuthFlagOnConstruction:
def test_minimax_init_does_not_flip_oauth(self):
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.anthropic_adapter.build_anthropic_client",
return_value=MagicMock()),
# Simulate a stale ANTHROPIC_TOKEN in the env — the init code
@@ -16,7 +16,7 @@ def _make_agent(api_max_retries=None):
if api_max_retries is not None:
cfg["agent"]["api_max_retries"] = api_max_retries
with patch("run_agent.OpenAI"), \
with patch("agent.process_bootstrap.OpenAI"), \
patch("hermes_cli.config.load_config", return_value=cfg), \
patch("hermes_cli.config.load_config_readonly", return_value=cfg):
return AIAgent(
@@ -24,9 +24,9 @@ from agent.turn_retry_state import TurnRetryState
def _make_agent(fallback_model=None):
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key",
@@ -75,9 +75,9 @@ def _tool_definition() -> dict:
def _make_loop_agent():
with (
patch("run_agent.get_tool_definitions", return_value=[_tool_definition()]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[_tool_definition()]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
patch("agent.model_metadata.get_model_context_length", return_value=256_000),
patch("agent.context_compressor.get_model_context_length", return_value=256_000),
):
+1 -1
View File
@@ -51,7 +51,7 @@ class TestCreateOpenAIClientCallable:
return MagicMock(api_key=kwargs.get("api_key"))
# Patch the module-level OpenAI proxy used by ``_create_openai_client``.
monkeypatch.setattr("run_agent.OpenAI", fake_openai)
monkeypatch.setattr("agent.process_bootstrap.OpenAI", fake_openai)
# Build a minimal stand-in for AIAgent so we can call the bound
# method directly without paying the full __init__ cost.
@@ -172,9 +172,9 @@ def _coherent_compressor() -> MagicMock:
@pytest.fixture()
def agent():
with (
patch("run_agent.get_tool_definitions", return_value=_make_tool_defs("web_search")),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=_make_tool_defs("web_search")),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
a = AIAgent(
api_key="test-key-1234567890",
@@ -229,7 +229,7 @@ def _run_marathon_turn(
patch.object(agent, "_save_trajectory"),
patch.object(agent, "_cleanup_task_resources"),
patch(
"run_agent.handle_function_call",
"model_tools.handle_function_call",
lambda name, args, task_id=None, **kwargs: json.dumps(
{"ok": True, "payload": BIG_TOOL_RESULT}
),
@@ -231,10 +231,10 @@ def test_init_feasibility_check_uses_aux_context_override_from_config():
with (
patch("hermes_cli.config.load_config", return_value=cfg), patch("hermes_cli.config.load_config_readonly", return_value=cfg),
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("run_agent.ContextCompressor", new=_StubCompressor),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
patch("agent.agent_init.ContextCompressor", new=_StubCompressor),
patch("agent.auxiliary_client.get_text_auxiliary_client", return_value=(mock_client, "custom/big-model")),
patch("agent.model_metadata.get_model_context_length", return_value=1_000_000) as mock_ctx_len,
):
@@ -27,14 +27,14 @@ def _no_compression_sleep(monkeypatch):
import time as _time
monkeypatch.setattr(_time, "sleep", lambda *_a, **_k: None)
monkeypatch.setattr(run_agent, "jittered_backoff", lambda *a, **k: 0.0)
monkeypatch.setattr("agent.retry_utils.jittered_backoff", lambda *a, **k: 0.0)
def _make_agent():
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key-1234567890",
@@ -18,11 +18,11 @@ import run_agent
def _patch_bootstrap(monkeypatch):
monkeypatch.setattr(run_agent, "get_tool_definitions", lambda **kwargs: [{
monkeypatch.setattr("model_tools.get_tool_definitions", lambda **kwargs: [{
"type": "function",
"function": {"name": "t", "description": "t", "parameters": {"type": "object", "properties": {}}},
}])
monkeypatch.setattr(run_agent, "check_toolset_requirements", lambda: {})
monkeypatch.setattr("model_tools.check_toolset_requirements", lambda: {})
class _FakeAnthropicClient:
@@ -20,9 +20,9 @@ from hermes_constants import PARTIAL_STREAM_STUB_ID, FINISH_REASON_LENGTH
def loop_agent():
from run_agent import AIAgent
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
a = AIAgent(
api_key="test-key-1234567890",
@@ -25,9 +25,9 @@ def loop_agent():
from run_agent import AIAgent
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
a = AIAgent(
api_key="test-key-1234567890",
@@ -66,9 +66,9 @@ def test_substantive_tool_only_turn_invalidates_older_housekeeping_fallback():
- Step 4 returns the nudge response as the final answer
"""
with (
patch("run_agent.get_tool_definitions", return_value=_tool_defs("todo", "web_search")),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=_tool_defs("todo", "web_search")),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key",
@@ -104,7 +104,7 @@ def test_substantive_tool_only_turn_invalidates_older_housekeeping_fallback():
]
with (
patch("run_agent.handle_function_call", return_value="ok"),
patch("model_tools.handle_function_call", return_value="ok"),
patch.object(agent, "_persist_session"),
patch.object(agent, "_save_trajectory"),
patch.object(agent, "_cleanup_task_resources"),
@@ -155,9 +155,9 @@ def test_bare_tool_marker_is_not_reused_as_final_response():
- Step 3 returns the nudge response as the final answer.
"""
with (
patch("run_agent.get_tool_definitions", return_value=_tool_defs("skill_manage")),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=_tool_defs("skill_manage")),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key",
@@ -187,7 +187,7 @@ def test_bare_tool_marker_is_not_reused_as_final_response():
]
with (
patch("run_agent.handle_function_call", return_value="ok"),
patch("model_tools.handle_function_call", return_value="ok"),
patch.object(agent, "_persist_session"),
patch.object(agent, "_save_trajectory"),
patch.object(agent, "_cleanup_task_resources"),
@@ -4,7 +4,7 @@ from run_agent import AIAgent
def _make_copilot_agent():
with patch("run_agent.OpenAI") as mock_openai:
with patch("agent.process_bootstrap.OpenAI") as mock_openai:
mock_openai.return_value = MagicMock()
agent = AIAgent(
api_key="gh-token",
@@ -13,7 +13,7 @@ from unittest.mock import MagicMock, patch
from run_agent import AIAgent
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_create_openai_client_disables_sdk_retries(mock_openai):
mock_openai.return_value = MagicMock()
agent = AIAgent(
@@ -46,7 +46,7 @@ def test_create_openai_client_disables_sdk_retries(mock_openai):
)
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_create_openai_client_honors_explicit_max_retries(mock_openai):
"""An explicit max_retries in client_kwargs is respected (setdefault, not
clobber) — future callers can opt back into SDK retries if needed."""
@@ -15,7 +15,7 @@ from unittest.mock import MagicMock, patch
from run_agent import AIAgent
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_create_openai_client_does_not_mutate_input_kwargs(mock_openai):
mock_openai.return_value = MagicMock()
agent = AIAgent(
@@ -14,7 +14,8 @@ from unittest.mock import patch
import httpx
from run_agent import AIAgent, _get_proxy_from_env, _get_proxy_for_base_url
from agent.process_bootstrap import _get_proxy_for_base_url, _get_proxy_from_env
from run_agent import AIAgent
def _make_agent():
@@ -60,7 +61,7 @@ def test_get_proxy_from_env_normalizes_socks_alias(monkeypatch):
assert _get_proxy_from_env() == "socks5://127.0.0.1:1080/"
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_create_openai_client_routes_via_proxy_when_env_set(mock_openai, monkeypatch):
"""With HTTPS_PROXY set, the custom httpx.Client must mount an HTTPProxy pool.
@@ -100,7 +101,7 @@ def test_create_openai_client_routes_via_proxy_when_env_set(mock_openai, monkeyp
http_client.close()
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_create_openai_client_no_proxy_when_env_unset(mock_openai, monkeypatch):
"""Without proxy env vars, no HTTPProxy mount should exist and
no custom socket-options transport should be installed."""
@@ -81,7 +81,7 @@ def test_second_create_does_not_wrap_closed_transport_from_first():
"base_url": "https://api.example.com/v1",
}
with patch("run_agent.OpenAI", fake_openai):
with patch("agent.process_bootstrap.OpenAI", fake_openai):
# Call 1 — what _replace_primary_openai_client does at init/rebuild.
client_a = agent._create_openai_client(
agent._client_kwargs, reason="initial", shared=True
@@ -156,7 +156,7 @@ def test_replace_primary_openai_client_survives_repeated_rebuilds():
"base_url": "https://api.example.com/v1",
}
with patch("run_agent.OpenAI", fake_openai):
with patch("agent.process_bootstrap.OpenAI", fake_openai):
# Seed the initial client so _replace has something to tear down.
agent.client = agent._create_openai_client(
agent._client_kwargs, reason="seed", shared=True
@@ -41,7 +41,7 @@ def test_rotate_immediately_when_credential_already_exhausted():
pool.mark_exhausted_and_rotate.return_value = entries[1]
from run_agent import AIAgent
with patch("run_agent.get_tool_definitions", return_value=[]), patch("run_agent.check_toolset_requirements", return_value={}), patch("run_agent.OpenAI"):
with patch("model_tools.get_tool_definitions", return_value=[]), patch("model_tools.check_toolset_requirements", return_value={}), patch("agent.process_bootstrap.OpenAI"):
agent = MagicMock(spec=AIAgent)
agent._credential_pool = pool
agent._swap_credential = MagicMock()
@@ -67,7 +67,7 @@ def test_rotate_on_second_429_when_not_exhausted():
pool.mark_exhausted_and_rotate.return_value = entries[1]
from run_agent import AIAgent
with patch("run_agent.get_tool_definitions", return_value=[]), patch("run_agent.check_toolset_requirements", return_value={}), patch("run_agent.OpenAI"):
with patch("model_tools.get_tool_definitions", return_value=[]), patch("model_tools.check_toolset_requirements", return_value={}), patch("agent.process_bootstrap.OpenAI"):
agent = MagicMock(spec=AIAgent)
agent._credential_pool = pool
agent._swap_credential = MagicMock()
@@ -26,7 +26,7 @@ _PROXY_CONFIG = {
}
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_custom_provider_extra_headers_applied_at_construction(mock_openai):
mock_openai.return_value = MagicMock()
with patch("hermes_cli.config.load_config", return_value=_PROXY_CONFIG):
+3 -3
View File
@@ -47,13 +47,13 @@ class _FakeClient:
def test_tool_call_validation_accepts_dict_arguments(monkeypatch):
from run_agent import AIAgent
monkeypatch.setattr("run_agent.OpenAI", lambda **kwargs: _FakeClient())
monkeypatch.setattr("agent.process_bootstrap.OpenAI", lambda **kwargs: _FakeClient())
monkeypatch.setattr(
"run_agent.get_tool_definitions",
"model_tools.get_tool_definitions",
lambda *args, **kwargs: [{"function": {"name": "read_file"}}],
)
monkeypatch.setattr(
"run_agent.handle_function_call",
"model_tools.handle_function_call",
lambda name, args, task_id=None, **kwargs: json.dumps({"ok": True, "args": args}),
)
@@ -30,9 +30,9 @@ def loop_agent():
``.chat.completions.create``."""
from run_agent import AIAgent
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key-1234567890",
@@ -157,7 +157,7 @@ class TestDroppedToolCallRecovery:
it to the durable transcript — a resumed session must not replay the
internal "issue the actual tool call now" instruction as user-authored
context (#69630 review follow-up)."""
from run_agent import _is_ephemeral_scaffolding
from agent.session_persistence import _is_ephemeral_scaffolding
from tests.run_agent.test_run_agent import _mock_response
loop_agent.client.chat.completions.create.side_effect = [
@@ -19,9 +19,9 @@ from run_agent import AIAgent
def _make_agent(fallback_model=None):
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key",
+3 -3
View File
@@ -50,11 +50,11 @@ def agent():
"""Minimal AIAgent with mocked OpenAI client and tool loading."""
with (
patch(
"run_agent.get_tool_definitions",
"model_tools.get_tool_definitions",
return_value=_make_tool_defs("web_search"),
),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
a = AIAgent(
api_key="test-key-1234567890",
@@ -29,9 +29,9 @@ CFG = {"agent": {}}
def _build_agent(model, base_url, provider="", max_tokens=None, window=131072):
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
patch("hermes_cli.config.load_config", return_value=CFG),
patch("hermes_cli.config.load_config_readonly", return_value=CFG),
patch(
@@ -6,7 +6,7 @@ verify that stripping preserves the role-alternation invariants providers
require, and that the phrase detector fires on the expected error bodies.
"""
from run_agent import _looks_like_image_content_rejection, _strip_images_from_messages
from agent.message_sanitization import _looks_like_image_content_rejection, _strip_images_from_messages
class TestStripImagesPreservesAlternation:
@@ -30,9 +30,9 @@ def test_init_tries_fallback_when_primary_returns_none():
return None, None # primary exhausted
with patch("agent.auxiliary_client.resolve_provider_client", side_effect=fake_resolve), \
patch("run_agent.get_tool_definitions", return_value=_make_tool_defs()), \
patch("run_agent.check_toolset_requirements", return_value={}), \
patch("run_agent.OpenAI", return_value=MagicMock()):
patch("model_tools.get_tool_definitions", return_value=_make_tool_defs()), \
patch("model_tools.check_toolset_requirements", return_value={}), \
patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
agent = AIAgent(
provider="alibaba-coding-plan",
@@ -52,9 +52,9 @@ def test_init_tries_fallback_when_primary_returns_none():
def test_init_raises_when_no_fallback_configured():
"""When primary returns None and no fallback is set, should raise."""
with patch("agent.auxiliary_client.resolve_provider_client", return_value=(None, None)), \
patch("run_agent.get_tool_definitions", return_value=_make_tool_defs()), \
patch("run_agent.check_toolset_requirements", return_value={}), \
patch("run_agent.OpenAI", return_value=MagicMock()):
patch("model_tools.get_tool_definitions", return_value=_make_tool_defs()), \
patch("model_tools.check_toolset_requirements", return_value={}), \
patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
with pytest.raises(RuntimeError, match="no API key was found"):
AIAgent(
@@ -88,9 +88,9 @@ def test_init_tries_fallback_when_openrouter_pool_exhausted():
return None, None # openrouter pool exhausted
with patch("agent.auxiliary_client.resolve_provider_client", side_effect=fake_resolve), \
patch("run_agent.get_tool_definitions", return_value=_make_tool_defs()), \
patch("run_agent.check_toolset_requirements", return_value={}), \
patch("run_agent.OpenAI", return_value=MagicMock()):
patch("model_tools.get_tool_definitions", return_value=_make_tool_defs()), \
patch("model_tools.check_toolset_requirements", return_value={}), \
patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
agent = AIAgent(
provider="openrouter",
@@ -115,9 +115,9 @@ def test_init_openrouter_exhausted_without_chain_keeps_generic_error():
"""openrouter exhausted + no usable fallback keeps the generic
'No LLM provider configured' error (not the named-provider one)."""
with patch("agent.auxiliary_client.resolve_provider_client", return_value=(None, None)), \
patch("run_agent.get_tool_definitions", return_value=_make_tool_defs()), \
patch("run_agent.check_toolset_requirements", return_value={}), \
patch("run_agent.OpenAI", return_value=MagicMock()):
patch("model_tools.get_tool_definitions", return_value=_make_tool_defs()), \
patch("model_tools.check_toolset_requirements", return_value={}), \
patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
with pytest.raises(RuntimeError, match="No LLM provider configured"):
AIAgent(
@@ -23,7 +23,8 @@ log = logging.getLogger("interrupt_test")
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from unittest.mock import MagicMock, patch
from run_agent import AIAgent, IterationBudget
from agent.iteration_budget import IterationBudget
from run_agent import AIAgent
from tools.interrupt import set_interrupt
def make_slow_response(delay=2.0):
@@ -99,7 +100,7 @@ def main() -> int:
"""Simulates the agent_thread in cli.py's chat() method."""
log.info("🟢 agent_thread starting")
with patch("run_agent.OpenAI") as MockOpenAI:
with patch("agent.process_bootstrap.OpenAI") as MockOpenAI:
mock_client = MagicMock()
mock_client.chat.completions.create = make_slow_response(delay=3.0)
mock_client.close = MagicMock()
@@ -64,8 +64,8 @@ def _make_agent(monkeypatch, completions):
from run_agent import AIAgent
client = SimpleNamespace(chat=SimpleNamespace(completions=completions))
monkeypatch.setattr("run_agent.OpenAI", lambda **kwargs: client)
monkeypatch.setattr("run_agent.get_tool_definitions", lambda *a, **k: [])
monkeypatch.setattr("agent.process_bootstrap.OpenAI", lambda **kwargs: client)
monkeypatch.setattr("model_tools.get_tool_definitions", lambda *a, **k: [])
agent = AIAgent(
model="test-model",
@@ -15,9 +15,9 @@ def _build_agent(model_cfg, custom_providers=None, model=None):
patch("hermes_cli.config.load_config", return_value=cfg),
patch("hermes_cli.config.load_config_readonly", return_value=cfg),
patch("agent.model_metadata.get_model_context_length", return_value=128_000),
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
from run_agent import AIAgent
@@ -14,7 +14,7 @@ def test_iteration_budget_used_is_thread_safe():
so a concurrent `consume()` could observe a partially-updated value or
cause the C-level `list.append` to raise a ValueError ("list size changed").
"""
from run_agent import IterationBudget
from agent.iteration_budget import IterationBudget
budget = IterationBudget(max_total=1000)
num_threads = 10
@@ -52,7 +52,7 @@ def test_iteration_budget_used_is_thread_safe():
def test_iteration_budget_consume_returns_false_when_exhausted():
"""consume() must return False once the budget is exhausted."""
from run_agent import IterationBudget
from agent.iteration_budget import IterationBudget
budget = IterationBudget(max_total=3)
assert budget.consume() is True
@@ -63,7 +63,7 @@ def test_iteration_budget_consume_returns_false_when_exhausted():
def test_iteration_budget_refund_restores_consume():
"""refund() after consume() must allow one more consume()."""
from run_agent import IterationBudget
from agent.iteration_budget import IterationBudget
budget = IterationBudget(max_total=2)
assert budget.consume() is True
@@ -75,7 +75,7 @@ def test_iteration_budget_refund_restores_consume():
def test_iteration_budget_used_reflects_consume_and_refund():
"""used property must accurately reflect consume() and refund() calls."""
from run_agent import IterationBudget
from agent.iteration_budget import IterationBudget
budget = IterationBudget(max_total=10)
@@ -92,7 +92,7 @@ def test_iteration_budget_used_reflects_consume_and_refund():
def test_iteration_budget_remaining():
"""remaining property must equal max_total - used."""
from run_agent import IterationBudget
from agent.iteration_budget import IterationBudget
budget = IterationBudget(max_total=5)
@@ -90,9 +90,9 @@ def loop_agent():
from run_agent import AIAgent
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
a = AIAgent(
api_key="test-key-1234567890",
@@ -21,10 +21,10 @@ def _make_agent() -> AIAgent:
}
]
with (
patch("run_agent.get_tool_definitions", return_value=tool_defs),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("model_tools.get_tool_definitions", return_value=tool_defs),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("hermes_cli.config.load_config", return_value={}),
patch("run_agent.OpenAI"),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key-1234567890",
@@ -78,7 +78,7 @@ def test_malformed_arguments_are_rejected_without_blocking_valid_sibling(
return json.dumps({"ok": args["query"]})
with (
patch("run_agent.handle_function_call", side_effect=fake_dispatch),
patch("model_tools.handle_function_call", side_effect=fake_dispatch),
patch.object(agent, "_invoke_tool", side_effect=fake_dispatch),
patch(
"agent.tool_executor.maybe_persist_tool_result",
+6 -6
View File
@@ -57,9 +57,9 @@ def test_blank_memory_provider_does_not_auto_enable_honcho():
) as from_global_config,
patch("plugins.memory.load_memory_provider") as load_memory_provider,
patch("agent.model_metadata.get_model_context_length", return_value=204_800),
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
from run_agent import AIAgent
@@ -101,9 +101,9 @@ def test_aiagent_forwards_user_id_alt_to_memory_provider():
patch("hermes_cli.config.load_config", return_value=cfg), patch("hermes_cli.config.load_config_readonly", return_value=cfg),
patch("plugins.memory.load_memory_provider", return_value=provider),
patch("agent.model_metadata.get_model_context_length", return_value=204_800),
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
from run_agent import AIAgent
+10 -10
View File
@@ -37,7 +37,7 @@ def _build_agent(config):
)
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_streaming_false_seeds_disable_streaming(mock_openai):
mock_openai.return_value = MagicMock()
agent = _build_agent({"model": {**_BASE["model"], "streaming": False}})
@@ -45,7 +45,7 @@ def test_streaming_false_seeds_disable_streaming(mock_openai):
assert agent._disable_streaming is True
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_streaming_absent_keeps_streaming_enabled(mock_openai):
mock_openai.return_value = MagicMock()
agent = _build_agent(_BASE)
@@ -53,7 +53,7 @@ def test_streaming_absent_keeps_streaming_enabled(mock_openai):
assert agent._disable_streaming is False
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_streaming_true_keeps_streaming_enabled(mock_openai):
mock_openai.return_value = MagicMock()
agent = _build_agent({"model": {**_BASE["model"], "streaming": True}})
@@ -61,7 +61,7 @@ def test_streaming_true_keeps_streaming_enabled(mock_openai):
assert agent._disable_streaming is False
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_streaming_string_false_seeds_disable_streaming(mock_openai):
"""String falsy values ('false', '0') must also disable streaming —
YAML users commonly quote booleans."""
@@ -71,7 +71,7 @@ def test_streaming_string_false_seeds_disable_streaming(mock_openai):
assert agent._disable_streaming is True
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_streaming_zero_seeds_disable_streaming(mock_openai):
mock_openai.return_value = MagicMock()
agent = _build_agent({"model": {**_BASE["model"], "streaming": 0}})
@@ -79,7 +79,7 @@ def test_streaming_zero_seeds_disable_streaming(mock_openai):
assert agent._disable_streaming is True
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_streaming_invalid_value_keeps_streaming_enabled(mock_openai):
"""Unrecognized values warn and keep the safe default (streaming on),
rather than silently disabling or crashing init."""
@@ -89,7 +89,7 @@ def test_streaming_invalid_value_keeps_streaming_enabled(mock_openai):
assert agent._disable_streaming is False
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_missing_model_section_keeps_streaming_enabled(mock_openai):
mock_openai.return_value = MagicMock()
agent = _build_agent({})
@@ -97,7 +97,7 @@ def test_missing_model_section_keeps_streaming_enabled(mock_openai):
assert agent._disable_streaming is False
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_legacy_string_model_section_does_not_crash(mock_openai):
"""The top-level ``model`` key is a legacy string; init must not crash."""
mock_openai.return_value = MagicMock()
@@ -106,7 +106,7 @@ def test_legacy_string_model_section_does_not_crash(mock_openai):
assert agent._disable_streaming is False
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_streaming_false_applies_to_every_agent_built_from_config(mock_openai):
"""Delegate children are constructed through the same init, so any agent
(parent or subagent) built under this config gets the escape hatch —
@@ -121,7 +121,7 @@ def test_streaming_false_applies_to_every_agent_built_from_config(mock_openai):
assert second._disable_streaming is True
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_streaming_false_read_from_real_config_file(mock_openai):
"""End-to-end: a real config.yaml in HERMES_HOME (sandboxed per-test by
conftest) with ``model.streaming: false`` must seed the flag through the
@@ -55,9 +55,9 @@ def _make_agent() -> AIAgent:
# validation ``ValueError`` *before* any API call, so the test passed
# without ever touching the 403 summarization path.
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
a = AIAgent(
api_key="test-key-1234567890",
@@ -14,9 +14,9 @@ from run_agent import AIAgent
def _make_agent(fallback_model=None):
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key",
@@ -80,7 +80,7 @@ def test_retry_after_api_connection_error_recreates_request_client(monkeypatch):
first_request = FakeRequestClient(lambda **kwargs: (_ for _ in ()).throw(_connection_error()))
second_request = FakeRequestClient(lambda **kwargs: {"ok": True})
factory = OpenAIFactory([first_request, second_request])
monkeypatch.setattr(run_agent, "OpenAI", factory)
monkeypatch.setattr("agent.process_bootstrap.OpenAI", factory)
agent = _build_agent()
@@ -106,7 +106,7 @@ def test_stale_non_stream_close_is_single_owner(monkeypatch):
request_client = FakeRequestClient(slow_responder)
factory = OpenAIFactory([request_client])
monkeypatch.setattr(run_agent, "OpenAI", factory)
monkeypatch.setattr("agent.process_bootstrap.OpenAI", factory)
agent = _build_agent()
agent._compute_non_stream_stale_timeout = lambda api_payload: 0.01
@@ -124,7 +124,7 @@ def test_closed_shared_client_is_recreated_before_request(monkeypatch):
replacement_shared = FakeSharedClient(lambda **kwargs: {"replacement": True})
request_client = FakeRequestClient(lambda **kwargs: {"ok": "fresh-request-client"})
factory = OpenAIFactory([replacement_shared, request_client])
monkeypatch.setattr(run_agent, "OpenAI", factory)
monkeypatch.setattr("agent.process_bootstrap.OpenAI", factory)
agent = _build_agent(shared_client=stale_shared)
result = agent._interruptible_api_call({"model": agent.model, "messages": []})
@@ -154,7 +154,7 @@ def test_concurrent_requests_do_not_break_each_other_when_one_client_closes(monk
first_client = FakeRequestClient(first_responder)
second_client = FakeRequestClient(second_responder)
factory = OpenAIFactory([first_client, second_client])
monkeypatch.setattr(run_agent, "OpenAI", factory)
monkeypatch.setattr("agent.process_bootstrap.OpenAI", factory)
agent = _build_agent()
results = {}
@@ -197,7 +197,7 @@ def test_streaming_call_recreates_closed_shared_client_before_request(monkeypatc
replacement_shared = FakeSharedClient(lambda **kwargs: {"replacement": True})
request_client = FakeRequestClient(lambda **kwargs: chunks)
factory = OpenAIFactory([replacement_shared, request_client])
monkeypatch.setattr(run_agent, "OpenAI", factory)
monkeypatch.setattr("agent.process_bootstrap.OpenAI", factory)
agent = _build_agent(shared_client=stale_shared)
agent.stream_delta_callback = lambda _delta: None
@@ -41,7 +41,7 @@ def _zen_call_headers(mock_openai):
return dict(matching[-1].kwargs.get("default_headers") or {})
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_opencode_free_blanks_authorization_header(mock_openai):
"""Whatever api_key value reaches the client build (placeholder, stale
key, empty), the Authorization default header must be blanked so the
@@ -62,7 +62,7 @@ def test_opencode_free_blanks_authorization_header(mock_openai):
)
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_opencode_free_sends_hermes_attribution(mock_openai):
"""Keyless requests still identify as Hermes (attribution headers match
the opencode zen/go profiles)."""
@@ -78,7 +78,7 @@ def test_opencode_free_sends_hermes_attribution(mock_openai):
assert str(headers.get("User-Agent", "")).startswith("HermesAgent/")
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_other_providers_unaffected(mock_openai):
"""The opencode-free header policy must not leak to other providers."""
mock_openai.return_value = MagicMock()
@@ -391,9 +391,9 @@ def loop_agent():
so we can stage a stub + continuation pair on .chat.completions.create."""
from run_agent import AIAgent
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
a = AIAgent(
api_key="test-key-1234567890",
@@ -674,9 +674,9 @@ class TestBuildAssistantMessageEmptyContentPad:
def _agent_for_builder(self):
from run_agent import AIAgent
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
a = AIAgent(
api_key="test-key-1234567890",
@@ -62,9 +62,9 @@ def test_plugin_engine_gets_model_thresholds_before_initial_update_model():
patch("hermes_cli.config.load_config", return_value=cfg), patch("hermes_cli.config.load_config_readonly", return_value=cfg),
patch("plugins.context_engine.load_context_engine", return_value=engine),
patch("agent.model_metadata.get_model_context_length", return_value=1_000_000),
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
from run_agent import AIAgent
@@ -48,9 +48,9 @@ def test_plugin_engine_gets_context_length_on_init():
patch("hermes_cli.config.load_config", return_value=cfg), patch("hermes_cli.config.load_config_readonly", return_value=cfg),
patch("plugins.context_engine.load_context_engine", return_value=engine),
patch("agent.model_metadata.get_model_context_length", return_value=204_800),
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
from run_agent import AIAgent
@@ -85,9 +85,9 @@ def test_active_context_engine_tools_survive_explicit_platform_toolsets():
patch("hermes_cli.config.load_config", return_value=cfg), patch("hermes_cli.config.load_config_readonly", return_value=cfg),
patch("plugins.context_engine.load_context_engine", return_value=engine),
patch("agent.model_metadata.get_model_context_length", return_value=204_800),
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
from run_agent import AIAgent
@@ -118,9 +118,9 @@ def test_plugin_engine_update_model_args():
patch("hermes_cli.config.load_config", return_value=cfg), patch("hermes_cli.config.load_config_readonly", return_value=cfg),
patch("plugins.context_engine.load_context_engine", return_value=engine),
patch("agent.model_metadata.get_model_context_length", return_value=131_072),
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
from run_agent import AIAgent
@@ -171,9 +171,9 @@ def test_codex_gpt55_autoraise_suppressed_for_plugin_engine():
patch("hermes_cli.config.load_config", return_value=cfg), patch("hermes_cli.config.load_config_readonly", return_value=cfg),
patch("plugins.context_engine.load_context_engine", return_value=engine),
patch("agent.model_metadata.get_model_context_length", return_value=272_000),
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
from run_agent import AIAgent
@@ -196,9 +196,9 @@ def test_codex_gpt55_autoraise_still_applies_to_builtin_compressor():
with (
patch("hermes_cli.config.load_config", return_value=cfg), patch("hermes_cli.config.load_config_readonly", return_value=cfg),
patch("agent.context_compressor.get_model_context_length", return_value=272_000),
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
from run_agent import AIAgent
@@ -95,9 +95,9 @@ def _pressured_compressor() -> MagicMock:
@pytest.fixture()
def agent():
with (
patch("run_agent.get_tool_definitions", return_value=_make_tool_defs("web_search")),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=_make_tool_defs("web_search")),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
a = AIAgent(
api_key="test-key-1234567890",
@@ -136,7 +136,7 @@ def _run_tool_loop(agent, n_tool_iterations: int):
patch.object(agent, "_save_trajectory"),
patch.object(agent, "_cleanup_task_resources"),
patch(
"run_agent.handle_function_call",
"model_tools.handle_function_call",
lambda name, args, task_id=None, **kwargs: json.dumps({"ok": True}),
),
):
@@ -68,9 +68,9 @@ def _make_agent(monkeypatch, tmp_path: Path, *, max_attempts) -> AIAgent:
db = SessionDB(db_path=tmp_path / "state.db")
with (
contextlib.redirect_stdout(io.StringIO()),
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
base_url="https://openrouter.ai/api/v1",
@@ -19,9 +19,9 @@ def test_turn_start_preflight_timeout_returns_typed_result_not_exception():
from run_agent import AIAgent
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key-1234567890",
+31 -31
View File
@@ -38,9 +38,9 @@ def _make_agent(
):
"""Create a minimal AIAgent with optional fallback config."""
with (
patch("run_agent.get_tool_definitions", return_value=_make_tool_defs("web_search")),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=_make_tool_defs("web_search")),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
# Unit tests must not probe live endpoints. The compressor resolves
# context length lazily via a real network call against base_url; for
# reachable hosts (the nous portal case) the endpoint's answer for the
@@ -109,9 +109,9 @@ class TestPrimaryRuntimeSnapshot:
def test_snapshot_includes_anthropic_state_when_applicable(self):
"""Anthropic-mode agents should snapshot Anthropic-specific state."""
with (
patch("run_agent.get_tool_definitions", return_value=_make_tool_defs("web_search")),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=_make_tool_defs("web_search")),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
patch("agent.anthropic_adapter.build_anthropic_client", return_value=MagicMock()),
):
agent = AIAgent(
@@ -161,7 +161,7 @@ class TestRestorePrimaryRuntime:
assert agent.provider == "openrouter"
# Restore should bring back the primary
with patch("run_agent.OpenAI", return_value=MagicMock()):
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
result = agent._restore_primary_runtime()
assert result is True
@@ -186,7 +186,7 @@ class TestRestorePrimaryRuntime:
emitted = []
agent._emit_status = emitted.append
with patch("run_agent.OpenAI", return_value=MagicMock()):
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
assert agent._restore_primary_runtime() is True
assert emitted == [
@@ -203,7 +203,7 @@ class TestRestorePrimaryRuntime:
emitted = []
agent._emit_status = emitted.append
with patch("run_agent.OpenAI", return_value=MagicMock()):
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
assert agent._restore_primary_runtime() is True
assert emitted == []
@@ -224,7 +224,7 @@ class TestRestorePrimaryRuntime:
emitted = []
agent._emit_status = emitted.append
with (
patch("run_agent.OpenAI", return_value=MagicMock()),
patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()),
patch.object(
agent.context_compressor,
"update_model",
@@ -254,7 +254,7 @@ class TestRestorePrimaryRuntime:
assert agent._fallback_index == 1 # consumed one entry
with patch("run_agent.OpenAI", return_value=MagicMock()):
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
agent._restore_primary_runtime()
assert agent._fallback_index == 0 # reset for next turn
@@ -275,7 +275,7 @@ class TestRestorePrimaryRuntime:
agent.context_compressor.context_length = 32000
agent.context_compressor.threshold_tokens = 25600
with patch("run_agent.OpenAI", return_value=MagicMock()):
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
agent._restore_primary_runtime()
assert agent.context_compressor.context_length == original_ctx_len
@@ -289,7 +289,7 @@ class TestRestorePrimaryRuntime:
agent._fallback_activated = True
agent._use_prompt_caching = not original_caching
with patch("run_agent.OpenAI", return_value=MagicMock()):
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
agent._restore_primary_runtime()
assert agent._use_prompt_caching == original_caching
@@ -300,7 +300,7 @@ class TestRestorePrimaryRuntime:
agent._fallback_activated = True
agent.request_overrides = {"extra_body": {"fallback_only": True}}
with patch("run_agent.OpenAI", return_value=MagicMock()):
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
result = agent._restore_primary_runtime()
assert result is True
@@ -339,7 +339,7 @@ class TestRestorePrimaryRuntime:
agent._credential_pool = _Pool()
agent._swap_credential = MagicMock()
with patch("run_agent.OpenAI", return_value=MagicMock()):
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
result = agent._restore_primary_runtime()
assert result is True
@@ -396,7 +396,7 @@ class TestRestorePrimaryRuntime:
primary_pool.provider = primary_provider
primary_pool.has_available.return_value = False
with (
patch("run_agent.OpenAI", return_value=MagicMock()),
patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()),
patch("agent.credential_pool.load_pool", return_value=primary_pool) as load_pool,
):
result = agent._restore_primary_runtime()
@@ -421,7 +421,7 @@ class TestRestorePrimaryRuntime:
agent._credential_pool = fallback_pool
with (
patch("run_agent.OpenAI", return_value=MagicMock()),
patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()),
patch(
"agent.credential_pool.load_pool",
side_effect=RuntimeError("auth store unavailable"),
@@ -464,7 +464,7 @@ class TestRestorePrimaryRuntime:
"agent.credential_pool.get_custom_provider_pool_key",
return_value="custom:myllm",
),
patch("run_agent.OpenAI", return_value=MagicMock()),
patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()),
):
result = agent._restore_primary_runtime()
@@ -511,7 +511,7 @@ class TestRestorePrimaryRuntime:
with (
patch("agent.credential_pool._load_config_safe", return_value=config),
patch("agent.credential_pool.load_pool", return_value=primary_pool) as load_pool,
patch("run_agent.OpenAI", return_value=MagicMock()),
patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()),
):
result = agent._restore_primary_runtime()
@@ -542,7 +542,7 @@ class TestRestorePrimaryRuntime:
with (
patch("agent.credential_pool._iter_custom_providers", return_value=configured),
patch("agent.credential_pool.load_pool", return_value=None) as load_pool,
patch("run_agent.OpenAI", return_value=MagicMock()),
patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()),
):
result = agent._restore_primary_runtime()
@@ -570,7 +570,7 @@ class TestTryRecoverPrimaryTransport:
agent = _make_agent(provider="custom")
error = _make_transport_error("ReadTimeout")
with patch("run_agent.OpenAI", return_value=MagicMock()), \
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()), \
patch("time.sleep"):
result = agent._try_recover_primary_transport(
error, retry_count=3, max_retries=3,
@@ -584,7 +584,7 @@ class TestTryRecoverPrimaryTransport:
error = _make_transport_error("ReadTimeout")
agent.request_overrides = {"extra_body": {"fallback_only": True}}
with patch("run_agent.OpenAI", return_value=MagicMock()), \
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()), \
patch("time.sleep"):
result = agent._try_recover_primary_transport(
error, retry_count=3, max_retries=3,
@@ -649,7 +649,7 @@ class TestTryRecoverPrimaryTransport:
agent = _make_agent(provider="custom")
error = _make_transport_error("ReadTimeout")
with patch("run_agent.OpenAI", return_value=MagicMock()), \
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()), \
patch("time.sleep") as mock_sleep:
agent._try_recover_primary_transport(
error, retry_count=3, max_retries=3,
@@ -661,7 +661,7 @@ class TestTryRecoverPrimaryTransport:
agent = _make_agent(provider="custom")
error = _make_transport_error("ReadTimeout")
with patch("run_agent.OpenAI", return_value=MagicMock()), \
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()), \
patch("time.sleep") as mock_sleep:
agent._try_recover_primary_transport(
error, retry_count=10, max_retries=3,
@@ -675,7 +675,7 @@ class TestTryRecoverPrimaryTransport:
agent = _make_agent(provider="custom")
error = _make_transport_error("ReadTimeout")
with patch("run_agent.OpenAI", side_effect=Exception("socket error")), \
with patch("agent.process_bootstrap.OpenAI", side_effect=Exception("socket error")), \
patch("time.sleep"):
result = agent._try_recover_primary_transport(
error, retry_count=3, max_retries=3,
@@ -720,7 +720,7 @@ class TestRestoreInRunConversation:
assert agent._fallback_index == 1
# Turn 2: restore primary
with patch("run_agent.OpenAI", return_value=MagicMock()):
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
assert agent._restore_primary_runtime() is True
assert agent._fallback_activated is False
@@ -757,7 +757,7 @@ class TestRateLimitCooldown:
def test_cooldown_set_on_rate_limit_reason(self):
"""_try_activate_fallback with rate_limit reason sets _rate_limited_until."""
from run_agent import FailoverReason
from agent.error_classifier import FailoverReason
agent = _make_agent(
fallback_model={"provider": "openrouter", "model": "anthropic/claude-sonnet-4"},
)
@@ -771,7 +771,7 @@ class TestRateLimitCooldown:
def test_cooldown_not_set_when_already_on_fallback(self):
"""Chain-switching while already on fallback must not reset cooldown."""
from run_agent import FailoverReason
from agent.error_classifier import FailoverReason
agent = _make_agent(
fallback_model=[
{"provider": "openrouter", "model": "model-a"},
@@ -806,7 +806,7 @@ class TestSwitchModelRequestOverridesSnapshot:
from agent.agent_runtime_helpers import switch_model
with (
patch("run_agent.OpenAI", return_value=MagicMock()),
patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()),
patch(
"agent.model_metadata.get_model_context_length",
return_value=128_000,
@@ -842,7 +842,7 @@ class TestSwitchModelRequestOverridesSnapshot:
# A fallback activation mid-turn clobbers the live overrides…
agent.request_overrides = {"extra_body": {"fallback_only": True}}
error = _make_transport_error("ReadTimeout")
with patch("run_agent.OpenAI", return_value=MagicMock()), \
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()), \
patch("time.sleep"):
result = agent._try_recover_primary_transport(
error, retry_count=3, max_retries=3,
@@ -862,7 +862,7 @@ class TestSwitchModelRequestOverridesSnapshot:
)
agent._fallback_activated = True
agent.request_overrides = {"extra_body": {"fallback_only": True}}
with patch("run_agent.OpenAI", return_value=MagicMock()):
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
result = agent._restore_primary_runtime()
assert result is True
assert agent.request_overrides == overrides
@@ -9,7 +9,7 @@ from unittest.mock import MagicMock, patch
from run_agent import AIAgent
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_openrouter_base_url_applies_or_headers(mock_openai):
mock_openai.return_value = MagicMock()
agent = AIAgent(
@@ -28,7 +28,7 @@ def test_openrouter_base_url_applies_or_headers(mock_openai):
assert headers["X-Title"] == "Hermes Agent"
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_ai_gateway_base_url_applies_attribution_headers(mock_openai):
mock_openai.return_value = MagicMock()
agent = AIAgent(
@@ -48,7 +48,7 @@ def test_ai_gateway_base_url_applies_attribution_headers(mock_openai):
assert headers["User-Agent"].startswith("HermesAgent/")
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_routermint_base_url_applies_user_agent_header(mock_openai):
mock_openai.return_value = MagicMock()
agent = AIAgent(
@@ -66,7 +66,7 @@ def test_routermint_base_url_applies_user_agent_header(mock_openai):
assert headers["User-Agent"].startswith("HermesAgent/")
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_nvidia_cloud_base_url_applies_billing_origin_header(mock_openai):
mock_openai.return_value = MagicMock()
agent = AIAgent(
@@ -87,7 +87,7 @@ def test_nvidia_cloud_base_url_applies_billing_origin_header(mock_openai):
assert headers["X-BILLING-INVOKE-ORIGIN"] == "HermesAgent"
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_fireworks_applies_attribution_via_profile_fallback(mock_openai):
"""Fireworks has no host-specific branch — its attribution headers come
from the profile.default_headers fallback, the path a model switch
@@ -111,7 +111,7 @@ def test_fireworks_applies_attribution_via_profile_fallback(mock_openai):
assert headers["User-Agent"].startswith("HermesAgent/")
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_opencode_go_applies_attribution_via_profile_fallback(mock_openai):
"""OpenCode (Zen/Go) attributes traffic by header like OpenRouter does.
Without profile.default_headers the relay only sees the OpenAI SDK's
@@ -135,7 +135,7 @@ def test_opencode_go_applies_attribution_via_profile_fallback(mock_openai):
assert headers["User-Agent"].startswith("HermesAgent/")
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_opencode_zen_applies_attribution_via_profile_fallback(mock_openai):
mock_openai.return_value = MagicMock()
agent = AIAgent(
@@ -156,7 +156,7 @@ def test_opencode_zen_applies_attribution_via_profile_fallback(mock_openai):
assert headers["User-Agent"].startswith("HermesAgent/")
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_routed_client_preserves_openai_sdk_custom_headers(mock_openai):
mock_openai.return_value = MagicMock()
routed_client = SimpleNamespace(
@@ -191,7 +191,7 @@ def test_routed_client_preserves_openai_sdk_custom_headers(mock_openai):
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_openrouter_headers_include_response_cache_when_enabled(mock_openai):
"""When openrouter.response_cache is True, the cache header is injected."""
mock_openai.return_value = MagicMock()
@@ -222,7 +222,7 @@ def test_openrouter_headers_include_response_cache_when_enabled(mock_openai):
# ---------------------------------------------------------------------------
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_user_default_headers_override_sdk_user_agent(mock_openai):
"""``model.default_headers`` lets a custom endpoint swap the OpenAI SDK
User-Agent that some gateways/WAFs reject (the #40033 reproduction)."""
@@ -255,7 +255,7 @@ def test_user_default_headers_override_sdk_user_agent(mock_openai):
@patch("run_agent.OpenAI")
@patch("agent.process_bootstrap.OpenAI")
def test_openrouter_headers_no_cache_when_disabled(mock_openai):
"""When openrouter.response_cache is False, no cache headers are sent."""
mock_openai.return_value = MagicMock()
+3 -3
View File
@@ -17,9 +17,9 @@ from run_agent import AIAgent, _pool_may_recover_from_rate_limit
def _make_agent(fallback_model=None):
"""Create a minimal AIAgent with optional fallback config."""
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key",
+3 -3
View File
@@ -66,9 +66,9 @@ def _reset_auxiliary_provider_state():
def _make_agent(monkeypatch, provider, api_mode="chat_completions", base_url="https://openrouter.ai/api/v1", model=None):
monkeypatch.setattr("run_agent.get_tool_definitions", lambda **kw: _tool_defs("web_search", "terminal"))
monkeypatch.setattr("run_agent.check_toolset_requirements", lambda: {})
monkeypatch.setattr("run_agent.OpenAI", _FakeOpenAI)
monkeypatch.setattr("model_tools.get_tool_definitions", lambda **kw: _tool_defs("web_search", "terminal"))
monkeypatch.setattr("model_tools.check_toolset_requirements", lambda: {})
monkeypatch.setattr("agent.process_bootstrap.OpenAI", _FakeOpenAI)
kwargs = dict(
api_key="test-key",
base_url=base_url,
@@ -2,9 +2,7 @@
import json
from run_agent import _repair_tool_call_arguments
from agent.message_sanitization import _repair_tool_call_arguments
class TestRepairToolCallArguments:
"""Verify each repair stage in the pipeline."""
@@ -96,9 +96,9 @@ def _make_tool_defs(*names):
def _make_agent(fallback_model=None):
with (
patch("run_agent.get_tool_definitions", return_value=_make_tool_defs("web_search")),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=_make_tool_defs("web_search")),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key-12345678",
@@ -256,7 +256,7 @@ class TestResetAwareRestoreGate:
agent._credential_pool = _FakePool("custom", next_at=None)
with patch("run_agent.OpenAI", return_value=MagicMock()):
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
assert agent._restore_primary_runtime() is True
assert agent._fallback_activated is False
assert agent.model == original_model
@@ -269,7 +269,7 @@ class TestResetAwareRestoreGate:
agent._credential_pool = _FakePool("custom", next_at=time.time() - 5)
with patch("run_agent.OpenAI", return_value=MagicMock()):
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
assert agent._restore_primary_runtime() is True
assert agent._fallback_activated is False
@@ -281,7 +281,7 @@ class TestResetAwareRestoreGate:
agent._credential_pool = _FakePool("custom", raise_on_next=True)
with patch("run_agent.OpenAI", return_value=MagicMock()):
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
assert agent._restore_primary_runtime() is True
assert agent._fallback_activated is False
@@ -310,7 +310,7 @@ class TestResetAwareRestoreGate:
agent._credential_pool = _FakePool("custom", next_at=None)
with patch("run_agent.OpenAI", return_value=MagicMock()):
with patch("agent.process_bootstrap.OpenAI", return_value=MagicMock()):
assert agent._restore_primary_runtime() is True
def test_logs_wait_only_once(self, caplog):
@@ -18,14 +18,13 @@ def _no_codex_backoff(monkeypatch):
"""Short-circuit retry backoff so Codex retry tests don't block on real
wall-clock waits (5s jittered_backoff base delay + tight time.sleep loop)."""
import time as _time
monkeypatch.setattr(run_agent, "jittered_backoff", lambda *a, **k: 0.0)
monkeypatch.setattr("agent.retry_utils.jittered_backoff", lambda *a, **k: 0.0)
monkeypatch.setattr(_time, "sleep", lambda *_a, **_k: None)
def _patch_agent_bootstrap(monkeypatch):
monkeypatch.setattr(
run_agent,
"get_tool_definitions",
"model_tools.get_tool_definitions",
lambda **kwargs: [
{
"type": "function",
@@ -37,7 +36,7 @@ def _patch_agent_bootstrap(monkeypatch):
}
],
)
monkeypatch.setattr(run_agent, "check_toolset_requirements", lambda: {})
monkeypatch.setattr("model_tools.check_toolset_requirements", lambda: {})
def _build_agent(monkeypatch):
@@ -512,8 +511,8 @@ def _build_xai_agent_with_slash_enum_tool(monkeypatch):
}
]
monkeypatch.setattr(run_agent, "get_tool_definitions", _fake_get_tool_definitions)
monkeypatch.setattr(run_agent, "check_toolset_requirements", lambda: {})
monkeypatch.setattr("model_tools.get_tool_definitions", _fake_get_tool_definitions)
monkeypatch.setattr("model_tools.check_toolset_requirements", lambda: {})
agent = run_agent.AIAgent(
model="grok-4.3",
@@ -1352,7 +1351,7 @@ def test_try_refresh_codex_client_credentials_handles_xai_oauth(monkeypatch):
"hermes_cli.auth.resolve_xai_oauth_runtime_credentials",
_fake_resolve,
)
monkeypatch.setattr(run_agent, "OpenAI", _fake_openai)
monkeypatch.setattr("agent.process_bootstrap.OpenAI", _fake_openai)
existing = _ExistingClient()
agent.client = existing
@@ -1458,7 +1457,7 @@ def test_try_refresh_copilot_client_credentials_rebuilds_client(monkeypatch):
"hermes_cli.copilot_auth.get_copilot_api_token",
lambda _raw: ("tid=exchanged-ide-token", None),
)
monkeypatch.setattr(run_agent, "OpenAI", _fake_openai)
monkeypatch.setattr("agent.process_bootstrap.OpenAI", _fake_openai)
agent.client = _ExistingClient()
ok = agent._try_refresh_copilot_client_credentials()
@@ -1497,7 +1496,7 @@ def test_try_refresh_copilot_client_credentials_rebuilds_even_if_token_unchanged
"hermes_cli.copilot_auth.get_copilot_api_token",
lambda _raw: ("tid=fresh-exchanged", None),
)
monkeypatch.setattr(run_agent, "OpenAI", _fake_openai)
monkeypatch.setattr("agent.process_bootstrap.OpenAI", _fake_openai)
ok = agent._try_refresh_copilot_client_credentials()
@@ -1531,7 +1530,7 @@ def test_try_refresh_copilot_client_credentials_falls_back_when_exchange_unavail
lambda _raw: None,
)
monkeypatch.setattr("hermes_cli.copilot_auth.get_copilot_api_token", _boom)
monkeypatch.setattr(run_agent, "OpenAI", _fake_openai)
monkeypatch.setattr("agent.process_bootstrap.OpenAI", _fake_openai)
ok = agent._try_refresh_copilot_client_credentials()
+3 -3
View File
@@ -267,9 +267,9 @@ class TestProjectionStopsReheal:
from run_agent import AIAgent
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key-1234567890",
@@ -62,7 +62,7 @@ def _deterministic_worker_start(monkeypatch):
def _make_agent(tmp_path: Path) -> AIAgent:
with (
patch(
"run_agent.get_tool_definitions",
"model_tools.get_tool_definitions",
return_value=[
{
"type": "function",
@@ -74,8 +74,8 @@ def _make_agent(tmp_path: Path) -> AIAgent:
}
],
),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
patch("run_agent._hermes_home", tmp_path),
patch("agent.model_metadata.fetch_model_metadata", return_value={}),
):
@@ -143,7 +143,7 @@ def test_sequential_tool_timeout_emits_result_and_continues(tmp_path, monkeypatc
started = time.monotonic()
try:
with (
patch("run_agent.handle_function_call", side_effect=_dispatch),
patch("model_tools.handle_function_call", side_effect=_dispatch),
patch(
"agent.tool_executor._emit_terminal_post_tool_call",
side_effect=_capture_terminal_event,
@@ -266,7 +266,7 @@ def test_sequential_tool_interrupt_hides_lifecycle_cancel_detail(tmp_path, monke
try:
with (
patch("run_agent.handle_function_call", side_effect=_dispatch),
patch("model_tools.handle_function_call", side_effect=_dispatch),
patch(
"agent.tool_executor._emit_terminal_post_tool_call",
side_effect=lambda *_args, **kwargs: terminal_events.append(kwargs),
@@ -327,7 +327,7 @@ def test_sequential_timeout_does_not_cut_clarify_human_wait(
messages: list[dict] = []
started = time.monotonic()
with (
patch("run_agent.handle_function_call", side_effect=_dispatch),
patch("model_tools.handle_function_call", side_effect=_dispatch),
patch(
"agent.tool_executor._emit_terminal_post_tool_call",
side_effect=_capture_terminal_event,
+3 -3
View File
@@ -728,9 +728,9 @@ class TestLegacyHiddenPlaceholderWireSubstitution:
from run_agent import AIAgent
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key-1234567890",
@@ -60,9 +60,9 @@ def _make_fallback_agent(fallback_model):
"""Full-constructor agent for the fallback path, mirroring
tests/run_agent/test_24996_fallback_exhaustion_cooldown.py."""
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key",
+4 -4
View File
@@ -1594,8 +1594,8 @@ class TestCopilotACPStreamingDecision:
must detect ACP runtimes and route to _interruptible_api_call instead.
"""
@patch("run_agent.get_tool_definitions", return_value=[])
@patch("run_agent.check_toolset_requirements", return_value={})
@patch("model_tools.get_tool_definitions", return_value=[])
@patch("model_tools.check_toolset_requirements", return_value={})
@patch("agent.copilot_acp_client.CopilotACPClient")
def test_provider_name_triggers_non_streaming(
self, mock_acp_cls, _mock_check, _mock_tools
@@ -1625,8 +1625,8 @@ class TestCopilotACPStreamingDecision:
response = mock_non_stream({})
mock_stream.assert_not_called()
@patch("run_agent.get_tool_definitions", return_value=[])
@patch("run_agent.check_toolset_requirements", return_value={})
@patch("model_tools.get_tool_definitions", return_value=[])
@patch("model_tools.check_toolset_requirements", return_value={})
@patch("agent.copilot_acp_client.CopilotACPClient")
def test_acp_base_url_triggers_non_streaming(
self, mock_acp_cls, _mock_check, _mock_tools
@@ -13,9 +13,7 @@ unclosed brackets, Python None) don't kill the session.
import json
from run_agent import _repair_tool_call_arguments
from agent.message_sanitization import _repair_tool_call_arguments
class TestStreamingAssemblyRepair:
"""Verify that _repair_tool_call_arguments is applied to streaming tool
call arguments before they're assembled into mock_tool_calls.
@@ -37,9 +37,9 @@ class _FakeOpenAI:
def _make_agent(monkeypatch, provider, api_mode="chat_completions", base_url="https://openrouter.ai/api/v1"):
monkeypatch.setattr("run_agent.get_tool_definitions", lambda **kw: _tool_defs("web_search", "terminal"))
monkeypatch.setattr("run_agent.check_toolset_requirements", lambda: {})
monkeypatch.setattr("run_agent.OpenAI", _FakeOpenAI)
monkeypatch.setattr("model_tools.get_tool_definitions", lambda **kw: _tool_defs("web_search", "terminal"))
monkeypatch.setattr("model_tools.check_toolset_requirements", lambda: {})
monkeypatch.setattr("agent.process_bootstrap.OpenAI", _FakeOpenAI)
return AIAgent(
api_key="test",
base_url=base_url,
+3 -3
View File
@@ -48,9 +48,9 @@ def _make_direct_start_agent(
) -> AIAgent:
with (
patch("hermes_cli.config.load_config", return_value=cfg), patch("hermes_cli.config.load_config_readonly", return_value=cfg),
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
patch("agent.agent_init.ContextCompressor", new=_StubStartupCompressor),
):
return AIAgent(
@@ -31,9 +31,9 @@ def loop_agent():
``test_dropped_tool_call_recovery.py``."""
from run_agent import AIAgent
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key-1234567890",
@@ -18,9 +18,9 @@ def _mock_response(*, usage: dict, content: str = "done"):
def _make_agent(session_db, *, platform: str):
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.get_tool_definitions", return_value=[]),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
agent = AIAgent(
api_key="test-key",
+5 -5
View File
@@ -8,7 +8,7 @@ against the tool's JSON Schema before dispatch.
from unittest.mock import patch
from model_tools import (
from tools.arg_coercion import (
coerce_tool_args,
_coerce_value,
_coerce_number,
@@ -111,7 +111,7 @@ class TestCoerceToolArgs:
def test_coerces_integer_arg(self):
schema = self._mock_schema({"limit": {"type": "integer"}})
with patch("model_tools.registry.get_schema", return_value=schema):
with patch("tools.arg_coercion.registry.get_schema", return_value=schema):
args = {"limit": "10"}
result = coerce_tool_args("test_tool", args)
assert result["limit"] == 10
@@ -122,7 +122,7 @@ class TestCoerceToolArgs:
def test_leaves_already_correct_types(self):
schema = self._mock_schema({"limit": {"type": "integer"}})
with patch("model_tools.registry.get_schema", return_value=schema):
with patch("tools.arg_coercion.registry.get_schema", return_value=schema):
args = {"limit": 10}
result = coerce_tool_args("test_tool", args)
assert result["limit"] == 10
@@ -224,7 +224,7 @@ class TestCoerceToolArgsNested:
def test_array_elements_as_json_strings_are_parsed(self):
schema = self._array_of_objects_schema()
with patch("model_tools.registry.get_schema", return_value=schema):
with patch("tools.arg_coercion.registry.get_schema", return_value=schema):
args = {"items": ['{"id": "1", "content": "x"}']}
result = coerce_tool_args("test_tool", args)
assert result["items"] == [{"id": "1", "content": "x"}]
@@ -233,7 +233,7 @@ class TestCoerceToolArgsNested:
def test_string_subfield_with_json_content_preserved(self):
"""A string-typed sub-field whose value looks like JSON must NOT be parsed."""
schema = self._array_of_objects_schema()
with patch("model_tools.registry.get_schema", return_value=schema):
with patch("tools.arg_coercion.registry.get_schema", return_value=schema):
args = {"items": [{"id": "1", "content": '{"not": "parsed"}'}]}
result = coerce_tool_args("test_tool", args)
assert result["items"][0]["content"] == '{"not": "parsed"}'
@@ -383,11 +383,11 @@ def _make_tool_defs(*names: str) -> list:
def agent():
with (
patch(
"run_agent.get_tool_definitions",
"model_tools.get_tool_definitions",
return_value=_make_tool_defs("web_search", "terminal"),
),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
patch("model_tools.check_toolset_requirements", return_value={}),
patch("agent.process_bootstrap.OpenAI"),
):
a = AIAgent(
api_key="test-key-1234567890",
@@ -427,7 +427,7 @@ class TestSegmentedDispatchIntegration:
events.append(("end", name, kwargs["tool_call_id"]))
return json.dumps({"ok": name})
with patch("run_agent.handle_function_call", side_effect=fake_handle):
with patch("model_tools.handle_function_call", side_effect=fake_handle):
agent._execute_tool_calls(msg, messages, "task-1")
# One result per call, in emission order.
@@ -460,7 +460,7 @@ class TestSegmentedDispatchIntegration:
executed.append(kwargs["tool_call_id"])
return json.dumps({"ok": True})
with patch("run_agent.handle_function_call", side_effect=fake_handle):
with patch("model_tools.handle_function_call", side_effect=fake_handle):
agent._execute_tool_calls(msg, messages, "task-1")
assert [m["tool_call_id"] for m in messages] == ["s1", "s2", "t1", "s3", "s4"]
@@ -507,7 +507,7 @@ class TestSegmentedDispatchIntegration:
agent._interrupt_requested = True
return json.dumps({"ok": True})
with patch("run_agent.handle_function_call", side_effect=fake_handle):
with patch("model_tools.handle_function_call", side_effect=fake_handle):
agent._execute_tool_calls(msg, messages, "task-1")
# Every call still gets exactly one result, in order.
@@ -532,7 +532,7 @@ class TestSegmentedDispatchIntegration:
return json.dumps({"ok": True})
agent.steer("focus on the tests")
with patch("run_agent.handle_function_call", side_effect=fake_handle):
with patch("model_tools.handle_function_call", side_effect=fake_handle):
agent._execute_tool_calls(msg, messages, "task-1")
contents = [m["content"] for m in messages]
@@ -602,7 +602,7 @@ class TestSegmentedDispatchIntegration:
return "small"
with (
patch("run_agent.handle_function_call", side_effect=fake_handle),
patch("model_tools.handle_function_call", side_effect=fake_handle),
patch("agent.tool_executor._budget_for_agent", return_value=budget),
):
agent._execute_tool_calls(msg, messages, "task-1")

Some files were not shown because too many files have changed in this diff Show More