fix: backup provider failover bugs and tool input type safety (#392)

* fix: backup provider failover bugs and tool input type safety

* fix: Add maximum boundaries to agent_tools

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
This commit is contained in:
Rajat Ahuja 2026-02-18 11:55:29 -05:00 committed by GitHub
parent 233df802f1
commit 046935a6bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 33 additions and 15 deletions

View File

@ -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()

View File

@ -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