diff --git a/src/utils/agent_tools.py b/src/utils/agent_tools.py index 0d89fa8a..b93cdecc 100644 --- a/src/utils/agent_tools.py +++ b/src/utils/agent_tools.py @@ -28,6 +28,18 @@ from src.utils.types import get_current_iteration logger = logging.getLogger(__name__) +def _safe_int(value: Any, default: int) -> int: + """Coerce a tool input value to int, returning default on failure. + + LLMs sometimes pass non-numeric strings (e.g. 'Infinity') for integer + parameters which would crash ``min()`` comparisons. + """ + try: + return int(value) + except (TypeError, ValueError, OverflowError): + return default + + # Module-level lock registry for thread-safe observation creation. # Keyed by (workspace_name, observer, observed) to ensure all tool executors # operating on the same data share the same lock. @@ -1163,7 +1175,7 @@ async def _handle_get_recent_history( async def _handle_search_memory(ctx: ToolContext, tool_input: dict[str, Any]) -> str: """Handle search_memory tool.""" - top_k = min(tool_input.get("top_k", 20), 40) + top_k = min(_safe_int(tool_input.get("top_k"), 20), 40) query = tool_input["query"] try: query_embedding = await embedding_client.embed(query) @@ -1188,7 +1200,7 @@ async def _handle_search_memory(ctx: ToolContext, tool_input: dict[str, Any]) -> # doesn't short-circuit and think there's nothing here, we # automatically search the message history for relevant information. if ctx.agent_type == "dialectic": - limit = min(tool_input.get("top_k", 20), 20) + limit = min(_safe_int(tool_input.get("top_k"), 20), 20) snippets = await crud.search_messages( ctx.db, workspace_name=ctx.workspace_name, @@ -1243,7 +1255,7 @@ async def _handle_get_observation_context( async def _handle_search_messages(ctx: ToolContext, tool_input: dict[str, Any]) -> str: """Handle search_messages tool.""" query = tool_input["query"] - limit = min(tool_input.get("limit", 10), 20) # Cap at 20 + limit = min(_safe_int(tool_input.get("limit"), 10), 20) # Cap at 20 snippets = await crud.search_messages( ctx.db, workspace_name=ctx.workspace_name, @@ -1263,8 +1275,10 @@ async def _handle_grep_messages(ctx: ToolContext, tool_input: dict[str, Any]) -> text = tool_input.get("text", "") if not text: return "ERROR: 'text' parameter is required" - limit = min(tool_input.get("limit", 10), 30) # Cap at 30 - context_window = min(tool_input.get("context_window", 2), 2) # Cap context + limit = min(_safe_int(tool_input.get("limit"), 10), 30) # Cap at 30 + context_window = min( + _safe_int(tool_input.get("context_window"), 2), 2 + ) # Cap context snippets = await crud.grep_messages( ctx.db, @@ -1316,7 +1330,7 @@ async def _handle_get_messages_by_date_range( """Handle get_messages_by_date_range tool.""" after_date_str = tool_input.get("after_date") before_date_str = tool_input.get("before_date") - limit = min(tool_input.get("limit", 20), 20) + limit = min(_safe_int(tool_input.get("limit"), 20), 20) order = tool_input.get("order", "desc") after_date = _parse_date(after_date_str, "after_date") @@ -1373,8 +1387,8 @@ async def _handle_search_messages_temporal( after_date_str = tool_input.get("after_date") before_date_str = tool_input.get("before_date") - limit = min(tool_input.get("limit", 10), 10) - context_window = min(tool_input.get("context_window", 2), 2) + limit = min(_safe_int(tool_input.get("limit"), 10), 10) + context_window = min(_safe_int(tool_input.get("context_window"), 2), 2) after_date = _parse_date(after_date_str, "after_date") if isinstance(after_date, str): @@ -1418,7 +1432,7 @@ async def _handle_get_recent_observations( workspace_name=ctx.workspace_name, observer=ctx.observer, observed=ctx.observed, - limit=tool_input.get("limit", 10), + limit=min(_safe_int(tool_input.get("limit"), 10), 100), session_name=ctx.session_name if session_only else None, ) representation = Representation.from_documents(documents) @@ -1443,7 +1457,7 @@ async def _handle_get_most_derived_observations( workspace_name=ctx.workspace_name, observer=ctx.observer, observed=ctx.observed, - limit=tool_input.get("limit", 10), + limit=min(_safe_int(tool_input.get("limit"), 10), 100), ) representation = Representation.from_documents(documents) total_count = representation.len() diff --git a/src/utils/clients.py b/src/utils/clients.py index ae1beadd..99eefe2e 100644 --- a/src/utils/clients.py +++ b/src/utils/clients.py @@ -711,6 +711,8 @@ async def _execute_tool_loop( effective_tool_choice = tool_choice while iteration < max_tool_iterations: + # Reset attempt counter so each iteration starts with the primary provider + _current_attempt.set(1) logger.debug(f"Tool execution iteration {iteration + 1}/{max_tool_iterations}") # Truncate BEFORE making the API call to avoid context length errors @@ -961,8 +963,10 @@ async def _execute_tool_loop( _current_attempt.set(1) # Reset attempt counter async def _final_call() -> HonchoLLMCallResponse[Any]: - provider = llm_settings.PROVIDER - model = llm_settings.MODEL + # Use shared provider selection helper for backup failover support + provider, model, thinking_budget, gpt5_reasoning_effort, gpt5_verbosity = ( + get_provider_and_model() + ) client = CLIENTS.get(provider) if not client: @@ -978,9 +982,9 @@ async def _execute_tool_loop( json_mode, _get_effective_temperature(temperature), stop_seqs, - reasoning_effort, - verbosity, - thinking_budget_tokens, + gpt5_reasoning_effort, + gpt5_verbosity, + thinking_budget, False, None, # No tools None, # No tool_choice