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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/praisonai-agents/praisonaiagents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ def _get_lazy_cache():
'CachingConfig': ('praisonaiagents.config.feature_configs', 'CachingConfig'),
'HooksConfig': ('praisonaiagents.config.feature_configs', 'HooksConfig'),
'SkillsConfig': ('praisonaiagents.config.feature_configs', 'SkillsConfig'),
'ToolRetryConfig': ('praisonaiagents.config.feature_configs', 'ToolRetryConfig'),
'AutonomyConfig': ('praisonaiagents.agent.autonomy', 'AutonomyConfig'),
'EscalationStage': ('praisonaiagents.escalation.types', 'EscalationStage'),
'EscalationPipeline': ('praisonaiagents.escalation.pipeline', 'EscalationPipeline'),
Expand Down
25 changes: 25 additions & 0 deletions src/praisonai-agents/praisonaiagents/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ def __init__(
interrupt_controller: Optional['InterruptController'] = None, # G2: Cooperative cancellation
tool_search: Optional[Union[bool, str, Dict[str, Any], 'ToolSearchConfig']] = False, # Progressive tool disclosure
message_steering: Optional[Union[bool, 'MessageSteeringProtocol']] = False, # Real-time message steering during execution
tool_retry_config: Optional['ToolRetryConfig'] = None, # Automatic retry/backoff for tool failures

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the structure and read the relevant sections of the agent.py file
wc -l src/praisonai-agents/praisonaiagents/agent/agent.py

Repository: MervinPraison/PraisonAI

Length of output: 218


🏁 Script executed:

# Read around line 600 to see the parameter definition and TYPE_CHECKING imports
sed -n '1,30p' src/praisonai-agents/praisonaiagents/agent/agent.py

Repository: MervinPraison/PraisonAI

Length of output: 1164


🏁 Script executed:

# Read the parameter definition around line 600
sed -n '590,610p' src/praisonai-agents/praisonaiagents/agent/agent.py

Repository: MervinPraison/PraisonAI

Length of output: 2010


🏁 Script executed:

# Read documentation around lines 717-720
sed -n '710,730p' src/praisonai-agents/praisonaiagents/agent/agent.py

Repository: MervinPraison/PraisonAI

Length of output: 1607


🏁 Script executed:

# Read the resolver logic around lines 1522-1540
sed -n '1515,1545p' src/praisonai-agents/praisonaiagents/agent/agent.py

Repository: MervinPraison/PraisonAI

Length of output: 1702


🏁 Script executed:

# Search for ToolRetryConfig definition
fd -t f '*.py' src/praisonai-agents/praisonaiagents | xargs grep -l "class ToolRetryConfig"

Repository: MervinPraison/PraisonAI

Length of output: 449


🏁 Script executed:

# Check for clone_for_channel method
sed -n '1/,/def clone_for_channel/p' src/praisonai-agents/praisonaiagents/agent/agent.py | tail -100

Repository: MervinPraison/PraisonAI

Length of output: 213


🏁 Script executed:

# Find where clone_for_channel is defined
grep -n "def clone_for_channel" src/praisonai-agents/praisonaiagents/agent/agent.py

Repository: MervinPraison/PraisonAI

Length of output: 210


🏁 Script executed:

# Check the TYPE_CHECKING imports to see if ToolRetryConfig is already there
sed -n '1,50p' src/praisonai-agents/praisonaiagents/agent/agent.py | grep -A 20 "TYPE_CHECKING"

Repository: MervinPraison/PraisonAI

Length of output: 1056


🏁 Script executed:

# Read the clone_for_channel method starting at line 2198
sed -n '2198,2280p' src/praisonai-agents/praisonaiagents/agent/agent.py

Repository: MervinPraison/PraisonAI

Length of output: 4090


🏁 Script executed:

# Search for where ToolRetryConfig is imported in the file
grep -n "ToolRetryConfig" src/praisonai-agents/praisonaiagents/agent/agent.py

Repository: MervinPraison/PraisonAI

Length of output: 922


🏁 Script executed:

# Check if ToolRetryConfig exists in the config module
find src/praisonai-agents/praisonaiagents -name "*.py" -exec grep -l "class ToolRetryConfig" {} \;

Repository: MervinPraison/PraisonAI

Length of output: 224


🏁 Script executed:

# Look at the complete resolver section more carefully including context
sed -n '1500,1550p' src/praisonai-agents/praisonaiagents/agent/agent.py

Repository: MervinPraison/PraisonAI

Length of output: 2859


🏁 Script executed:

# Check if there's a TYPE_CHECKING block anywhere in the file
grep -n "if TYPE_CHECKING:" src/praisonai-agents/praisonaiagents/agent/agent.py

Repository: MervinPraison/PraisonAI

Length of output: 183


🏁 Script executed:

# Look for any ToolRetryConfig imports at the top of the file
grep -n "from.*ToolRetryConfig\|import.*ToolRetryConfig" src/praisonai-agents/praisonaiagents/agent/agent.py

Repository: MervinPraison/PraisonAI

Length of output: 323


🏁 Script executed:

# Check the complete clone_for_channel to see if tool_retry_config appears anywhere
sed -n '2198,2300p' src/praisonai-agents/praisonaiagents/agent/agent.py | grep -i "tool_retry"

Repository: MervinPraison/PraisonAI

Length of output: 161


🏁 Script executed:

# Check what ToolRetryConfig looks like
grep -A 15 "class ToolRetryConfig" src/praisonai-agents/praisonaiagents/config/feature_configs.py

Repository: MervinPraison/PraisonAI

Length of output: 786


🏁 Script executed:

# Check what's in the TYPE_CHECKING block around line 245
sed -n '245,280p' src/praisonai-agents/praisonaiagents/agent/agent.py

Repository: MervinPraison/PraisonAI

Length of output: 1780


🏁 Script executed:

# Check the full clone_for_channel method to see if tool_retry_config is mentioned
sed -n '2198,2350p' src/praisonai-agents/praisonaiagents/agent/agent.py

Repository: MervinPraison/PraisonAI

Length of output: 7203


🏁 Script executed:

# Search for all occurrences of tool_retry_config in the entire file to understand usage
grep -n "tool_retry_config" src/praisonai-agents/praisonaiagents/agent/agent.py

Repository: MervinPraison/PraisonAI

Length of output: 981


🏁 Script executed:

# Check if there are any linting errors or hints about the F821 error
rg "F821" src/praisonai-agents/praisonaiagents/agent/

Repository: MervinPraison/PraisonAI

Length of output: 161


🏁 Script executed:

# Check if there are any ruff or linting configuration files
find src/praisonai-agents/praisonaiagents -name "ruff.toml" -o -name ".ruff.toml" -o -name "pyproject.toml" | head -5

Repository: MervinPraison/PraisonAI

Length of output: 161


🏁 Script executed:

# Check what else is in the clone_kwargs to understand the pattern
sed -n '2220,2260p' src/praisonai-agents/praisonaiagents/agent/agent.py | grep "getattr"

Repository: MervinPraison/PraisonAI

Length of output: 1596


🏁 Script executed:

# Verify the exact line 720 to see the complete documentation
sed -n '717,730p' src/praisonai-agents/praisonaiagents/agent/agent.py

Repository: MervinPraison/PraisonAI

Length of output: 1098


🏁 Script executed:

# Check if there are attributes stored as self.tool_retry_config vs self._tool_retry_config
grep -n "self\.tool_retry_config\|self\._tool_retry_config" src/praisonai-agents/praisonaiagents/agent/agent.py

Repository: MervinPraison/PraisonAI

Length of output: 411


Add tool_retry_config to clone propagation and align type annotation/documentation with runtime behavior.

The tool_retry_config parameter accepts None, False, True, dict, and ToolRetryConfig instance at runtime (lines 1526–1540), but the type annotation (line 600) and docstring (line 717–720) only describe ToolRetryConfig instance. Additionally, clone_for_channel() omits tool_retry_config from the cloned configuration, causing state loss during agent cloning.

💡 Required fixes
  1. Update type annotation to document all accepted forms:
-        tool_retry_config: Optional['ToolRetryConfig'] = None,  # Automatic retry/backoff for tool failures
+        tool_retry_config: Optional[Union[bool, Dict[str, Any], 'ToolRetryConfig']] = None,  # Automatic retry/backoff for tool failures
  1. Update docstring to document all accepted types:
             tool_retry_config: Automatic retry with exponential backoff for transient tool failures.
                 When configured, retryable ToolExecutionErrors (network timeouts, rate limits,
                 external service errors) will automatically retry before surfacing to the model.
-                Default: None (no retry, preserves current behavior). Accepts ToolRetryConfig instance.
+                Default: None (no retry, preserves current behavior). Accepts:
+                - None/False: disabled
+                - True: default ToolRetryConfig
+                - Dict[str, Any]: ToolRetryConfig fields
+                - ToolRetryConfig: explicit configuration
  1. Add to clone_for_channel() kwargs (after line 2245):
             'tool_search': getattr(self, '_tool_search_config', None),
             # Tool configuration - use consolidated config when available  
             'tool_config': getattr(self, '_tool_config', None),
+            'tool_retry_config': getattr(self, 'tool_retry_config', None),
             
             # Retry configuration
             'retry': getattr(self, '_retry_config', None),
🧰 Tools
🪛 Ruff (0.15.18)

[error] 600-600: Undefined name ToolRetryConfig

(F821)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-agents/praisonaiagents/agent/agent.py` at line 600, The
tool_retry_config parameter has inconsistent type annotation and documentation
compared to its runtime behavior. Update the type annotation on the parameter
definition to use Union type to document that it accepts None, False, True,
dict, and ToolRetryConfig instance (not just Optional ToolRetryConfig). Update
the docstring to document all these accepted types. Additionally, the
clone_for_channel() method is missing tool_retry_config from its kwargs
propagation, so add tool_retry_config to the cloned agent configuration in the
clone_for_channel() method's kwargs to ensure the configuration is preserved
during agent cloning.

Source: Linters/SAST tools

sandbox: Optional[Union[bool, 'SandboxConfig']] = None, # Sandbox for safe code execution
retry: Optional[Union[bool, Dict[str, Any], 'RetryBackoffConfig']] = None, # Retry configuration with exponential backoff
):
Expand Down Expand Up @@ -713,6 +714,10 @@ def __init__(
When enabled, replaces large tool schemas with bridge tools (tool_search,
tool_describe, tool_call) to save context. Core SDK tools never defer.
Auto mode activates based on token threshold. Opt-in feature.
tool_retry_config: Automatic retry with exponential backoff for transient tool failures.
When configured, retryable ToolExecutionErrors (network timeouts, rate limits,
external service errors) will automatically retry before surfacing to the model.
Default: None (no retry, preserves current behavior). Accepts ToolRetryConfig instance.

Raises:
ValueError: If all of name, role, goal, backstory, and instructions are None.
Expand Down Expand Up @@ -1513,6 +1518,26 @@ def __init__(
"tool_search must be False/None, True, a mode string, "
"a dict of ToolSearchConfig fields, or ToolSearchConfig"
)

# ─────────────────────────────────────────────────────────────────────
# Resolve TOOL_RETRY_CONFIG param
# ─────────────────────────────────────────────────────────────────────
# Support bool/dict for API consistency with other params
if tool_retry_config is None or tool_retry_config is False:
self.tool_retry_config = None
elif tool_retry_config is True:
from ..config import ToolRetryConfig
self.tool_retry_config = ToolRetryConfig()
elif isinstance(tool_retry_config, dict):
from ..config import ToolRetryConfig
self.tool_retry_config = ToolRetryConfig(**tool_retry_config)
else:
# Validate it's a ToolRetryConfig instance
from ..config import ToolRetryConfig
if isinstance(tool_retry_config, ToolRetryConfig):
self.tool_retry_config = tool_retry_config
else:
raise TypeError("tool_retry_config must be None, bool, dict, or ToolRetryConfig instance")
Comment on lines +1522 to +1540

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve tool_retry_config when cloning agents.

self.tool_retry_config is now set here, but clone_for_channel() doesn’t pass it into the new Agent(...), so cloned agents silently lose tool retry/backoff behavior.

💡 Proposed fix (outside this hunk)
@@ def clone_for_channel(self) -> "Agent":
         clone_kwargs = {
@@
             # Retry configuration
             'retry': getattr(self, '_retry_config', None),
+            'tool_retry_config': getattr(self, 'tool_retry_config', None),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-agents/praisonaiagents/agent/agent.py` around lines 1522 -
1540, Locate the clone_for_channel() method in the Agent class and update the
creation of the new Agent instance to include the tool_retry_config parameter,
passing self.tool_retry_config as its value. This ensures that when agents are
cloned for a channel, the tool retry configuration is preserved from the
original agent to the cloned agent.


# Process tool_config and artifact storage (moved from tool_output)
self._artifact_store = None
Expand Down
288 changes: 133 additions & 155 deletions src/praisonai-agents/praisonaiagents/agent/tool_execution.py

Large diffs are not rendered by default.

132 changes: 132 additions & 0 deletions src/praisonai-agents/praisonaiagents/agent/tool_execution_fixed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""
Tool execution mixin for the Agent class.

Contains all methods for tool resolution, execution, approval,
cost tracking, and hook integration. Extracted from agent.py
for maintainability.
"""

import os
import time
import json
import logging
import asyncio
import inspect
import contextvars
import concurrent.futures
import random
from typing import List, Optional, Any, Dict, Union, TYPE_CHECKING
from ..errors import ToolExecutionError

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
pass


class ToolExecutionMixin:
"""Mixin providing tool execution methods for the Agent class."""

def _get_existing_stream_emitter(self):
"""Return an already-initialized stream emitter without creating one."""
emitter = getattr(self, "_stream_emitter", None)
if emitter is not None:
return emitter

# Support name-mangled private attributes across class renames/inheritance.
for cls in type(self).mro():
mangled = f"_{cls.__name__}__stream_emitter"
if hasattr(self, mangled):
emitter = getattr(self, mangled, None)
if emitter is not None:
return emitter
return None

def _resolve_tool_names(self, tool_names):
"""Resolve tool names to actual tool instances from registry.

Args:
tool_names: List of tool name strings

Returns:
List of resolved tool instances
"""
resolved = []
try:
from ..tools.registry import get_registry
registry = get_registry()

for name in tool_names:
tool = registry.get(name)
if tool is not None:
resolved.append(tool)
else:
logging.warning(f"Tool '{name}' not found in registry")
except ImportError:
logging.warning("Tool registry not available, cannot resolve tool names")

return resolved

def _cast_arguments(self, func, arguments):
"""Cast arguments to their expected types based on function signature."""
if not callable(func) or not arguments:
return arguments

try:
sig = inspect.signature(func)
valid_params = set(sig.parameters.keys()) - {'self'}
casted_args = {}

# Sanitize argument names: strip trailing '=', whitespace, and
# other invalid chars that LLMs sometimes hallucinate in kwarg names
sanitized = {}
for raw_name, arg_value in arguments.items():
clean = raw_name.strip().rstrip('=').strip()
# If the cleaned name matches a valid param, use it;
# otherwise try case-insensitive match
if clean in valid_params:
sanitized[clean] = arg_value
elif clean.lower() in {p.lower() for p in valid_params}:
# Case-insensitive fuzzy match
matched = next(p for p in valid_params if p.lower() == clean.lower())
sanitized[matched] = arg_value
else:
sanitized[clean] = arg_value
arguments = sanitized

for param_name, arg_value in arguments.items():
if param_name in sig.parameters:
param = sig.parameters[param_name]
if param.annotation != inspect.Parameter.empty:
# Try to cast to the expected type
annotation = param.annotation
# Handle Optional types
if hasattr(annotation, '__origin__'):
if annotation.__origin__ is Union:
# Get non-None type from Union
types = [t for t in annotation.__args__ if t != type(None)]
if types:
annotation = types[0]
Comment on lines +103 to +109

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify no remaining direct equality/inequality comparisons against type(None).
rg -n -C2 '!=\s*type\(None\)|==\s*type\(None\)'

Repository: MervinPraison/PraisonAI

Length of output: 161


🏁 Script executed:

#!/bin/bash
# Check if the file exists and read the relevant lines
if [ -f "src/praisonai-agents/praisonaiagents/agent/tool_execution_fixed.py" ]; then
  echo "=== File found, reading lines 100-115 ==="
  sed -n '100,115p' "src/praisonai-agents/praisonaiagents/agent/tool_execution_fixed.py" | cat -n
else
  echo "File not found at expected path. Searching for file..."
  find . -name "tool_execution_fixed.py" -type f 2>/dev/null
fi

Repository: MervinPraison/PraisonAI

Length of output: 1258


🏁 Script executed:

#!/bin/bash
# Also search more broadly for NoneType comparisons in the agent directory
echo "=== Searching for type(None) references in agent module ==="
rg 'type\(None\)' src/praisonai-agents/praisonaiagents/agent/ -n -B2 -A2

Repository: MervinPraison/PraisonAI

Length of output: 894


Use identity comparison for NoneType to fix Ruff E721 violation.

Line 107 uses != instead of is not when comparing against type(None). This should be refactored to use identity comparison for correctness and to comply with Ruff E721.

Proposed fix
-                                types = [t for t in annotation.__args__ if t != type(None)]
+                                none_type = type(None)
+                                types = [t for t in annotation.__args__ if t is not none_type]
🧰 Tools
🪛 Ruff (0.15.18)

[error] 107-107: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-agents/praisonaiagents/agent/tool_execution_fixed.py` around
lines 103 - 109, The list comprehension in the annotation handling block uses
`!=` comparison with `type(None)` instead of the identity comparison operator
`is not`. In the line where types are filtered from `annotation.__args__`,
replace the `!= type(None)` comparison with `is not type(None)` to comply with
Ruff E721 style guidelines and properly use identity comparison for type
checking.

Source: Linters/SAST tools


# Cast if it's a basic type
if annotation in (int, float, str, bool):
try:
if annotation is bool and isinstance(arg_value, str):
# Special handling for bool strings
casted_args[param_name] = arg_value.lower() in ('true', '1', 'yes')
else:
casted_args[param_name] = annotation(arg_value)
except (ValueError, TypeError):
casted_args[param_name] = arg_value
else:
casted_args[param_name] = arg_value
else:
casted_args[param_name] = arg_value
else:
# Keep unexpected parameters as is (function may use **kwargs)
casted_args[param_name] = arg_value

return casted_args
except Exception:
# If signature inspection fails, return arguments as is
return arguments
Comment on lines +27 to +132

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether tool_execution_fixed.py is wired anywhere and inspect duplicate ToolExecutionMixin definitions.
rg -n -C3 '\btool_execution_fixed\b|from\s+.*tool_execution_fixed|import\s+.*tool_execution_fixed'
rg -n -C3 '^class\s+ToolExecutionMixin\b'

Repository: MervinPraison/PraisonAI

Length of output: 161


🏁 Script executed:

#!/bin/bash
# Check if both files exist
echo "=== Checking file existence ==="
find . -name "tool_execution*.py" -type f

echo -e "\n=== Checking ToolExecutionMixin definitions ==="
fd -t f "tool_execution.*\.py" | while read f; do
  echo "File: $f"
  grep -n "^class ToolExecutionMixin" "$f" || echo "  (no ToolExecutionMixin class found)"
done

echo -e "\n=== Checking imports of tool_execution files ==="
rg "from.*tool_execution|import.*tool_execution" -t py

echo -e "\n=== Checking what imports ToolExecutionMixin ==="
rg "ToolExecutionMixin" -t py --no-heading

Repository: MervinPraison/PraisonAI

Length of output: 622


🏁 Script executed:

#!/bin/bash
# Check usage of ToolExecutionMixin and which file defines it in the Agent class
echo "=== Searching for ToolExecutionMixin usage ==="
rg "ToolExecutionMixin" -t py -B2 -A2

echo -e "\n=== Checking Agent class definition and inheritance ==="
rg "class Agent.*ToolExecutionMixin" -t py -B2 -A5

echo -e "\n=== Checking which file is imported in __init__.py ==="
cat ./src/praisonai-agents/praisonaiagents/agent/__init__.py 2>/dev/null || echo "File not found"

echo -e "\n=== Line 107 context in tool_execution_fixed.py ==="
sed -n '100,115p' ./src/praisonai-agents/praisonaiagents/agent/tool_execution_fixed.py

Repository: MervinPraison/PraisonAI

Length of output: 396


🏁 Script executed:

#!/bin/bash
# More detailed check on Agent class and what it actually inherits from
echo "=== Finding Agent class definition ==="
fd -t f "agent.py" "src/praisonai-agents/praisonaiagents/agent/" | xargs grep -l "^class Agent"

echo -e "\n=== Agent class definition ==="
fd -t f "agent.py" "src/praisonai-agents/praisonaiagents/agent/" | xargs head -100

echo -e "\n=== Check if tool_execution_fixed.py is referenced anywhere ==="
rg "tool_execution_fixed" -t py

echo -e "\n=== Comparing _cast_arguments method in both files ==="
echo "From tool_execution.py:"
sed -n '52,200p' ./src/praisonai-agents/praisonaiagents/agent/tool_execution.py | grep -A50 "_cast_arguments"

echo -e "\nFrom tool_execution_fixed.py:"
sed -n '27,132p' ./src/praisonai-agents/praisonaiagents/agent/tool_execution_fixed.py | grep -A50 "_cast_arguments"

Repository: MervinPraison/PraisonAI

Length of output: 49484


🏁 Script executed:

#!/bin/bash
# Check line 107 for the E721 violation
echo "=== Line 107 context (tool_execution_fixed.py) ==="
sed -n '100,115p' ./src/praisonai-agents/praisonaiagents/agent/tool_execution_fixed.py | cat -n

echo -e "\n=== Checking if tool_execution_fixed.py imports are anywhere ==="
rg "tool_execution_fixed" --glob "*.py"

echo -e "\n=== Verifying tool_execution.py import in agent.py ==="
grep -n "from.*tool_execution" ./src/praisonai-agents/praisonaiagents/agent/agent.py

Repository: MervinPraison/PraisonAI

Length of output: 1333


🏁 Script executed:

#!/bin/bash
# Get the exact import line from agent.py
echo "=== Tool execution import in agent.py ==="
grep -n "tool_execution" ./src/praisonai-agents/praisonaiagents/agent/agent.py

echo -e "\n=== Verify what _fixed file contains vs original tool_execution.py ==="
echo "Methods in tool_execution.py:"
rg "^\s{4}def " ./src/praisonai-agents/praisonaiagents/agent/tool_execution.py --no-heading | head -20

echo -e "\nMethods in tool_execution_fixed.py:"
rg "^\s{4}def " ./src/praisonai-agents/praisonaiagents/agent/tool_execution_fixed.py --no-heading | head -20

echo -e "\n=== Check git status to see if _fixed is uncommitted/merge artifact ==="
cd ./src/praisonai-agents/praisonaiagents/agent/ && git log --oneline -5 tool_execution_fixed.py 2>/dev/null || echo "No git history available"

echo -e "\n=== Check if _fixed has different implementation than tool_execution.py ==="
echo "tool_execution.py _cast_arguments signature:"
sed -n '52,200p' ./tool_execution.py | grep -A2 "def _cast_arguments"

echo -e "\ntool_execution_fixed.py _cast_arguments signature:"
sed -n '27,132p' ./tool_execution_fixed.py | grep -A2 "def _cast_arguments"

Repository: MervinPraison/PraisonAI

Length of output: 2809


🏁 Script executed:

#!/bin/bash
# Get the exact line 107-109 context to check the E721 issue
echo "=== Exact lines 105-112 with context ==="
sed -n '105,112p' ./src/praisonai-agents/praisonaiagents/agent/tool_execution_fixed.py | cat -n

echo -e "\n=== Verify the comparison issue at line 108 ==="
sed -n '108p' ./src/praisonai-agents/praisonaiagents/agent/tool_execution_fixed.py | cat -n

echo -e "\n=== Check if this same pattern exists in tool_execution.py ==="
grep -n "t != type(None)" ./src/praisonai-agents/praisonaiagents/agent/tool_execution.py

echo -e "\n=== Final confirmation: tool_execution_fixed.py is not imported/used anywhere ==="
rg "tool_execution_fixed" --type py -A2 -B2 2>/dev/null || echo "No references found (dead code confirmed)"

echo -e "\n=== Summary of tool_execution_fixed.py status ==="
echo "File size:"
wc -l ./src/praisonai-agents/praisonaiagents/agent/tool_execution_fixed.py

echo -e "\nTotal method count comparison:"
echo "tool_execution.py methods: $(grep -c '^\s*def ' ./src/praisonai-agents/praisonaiagents/agent/tool_execution.py)"
echo "tool_execution_fixed.py methods: $(grep -c '^\s*def ' ./src/praisonai-agents/praisonaiagents/agent/tool_execution_fixed.py)"

Repository: MervinPraison/PraisonAI

Length of output: 1414


Delete this file—it is dead code and a merge artifact.

tool_execution_fixed.py is not imported anywhere in the codebase; agent.py imports from the full tool_execution.py instead (line 20). This file contains only 3 partial helper methods and was created during a merge conflict resolution. Since the complete mixin is already available and actively used in tool_execution.py, this duplicate stub adds no value and creates maintenance burden. Remove it entirely.

🧰 Tools
🪛 Ruff (0.15.18)

[error] 107-107: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)


[warning] 130-130: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-agents/praisonaiagents/agent/tool_execution_fixed.py` around
lines 27 - 132, The file tool_execution_fixed.py is dead code and a merge
artifact that should be removed entirely. It is not imported anywhere in the
codebase; instead, agent.py (line 20) imports the complete mixin from
tool_execution.py. Since tool_execution_fixed.py contains only partial duplicate
helper methods and the full, actively-used mixin already exists in
tool_execution.py, delete the entire tool_execution_fixed.py file to eliminate
maintenance burden and reduce code duplication.

Source: Coding guidelines

2 changes: 2 additions & 0 deletions src/praisonai-agents/praisonaiagents/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"HooksConfig",
"SkillsConfig",
"ToolSearchConfig",
"ToolRetryConfig",
# Type aliases
"MemoryParam",
"KnowledgeParam",
Expand Down Expand Up @@ -126,6 +127,7 @@
"HooksConfig": "feature_configs",
"SkillsConfig": "feature_configs",
"ToolSearchConfig": "feature_configs",
"ToolRetryConfig": "feature_configs",
"MemoryParam": "feature_configs",
"KnowledgeParam": "feature_configs",
"PlanningParam": "feature_configs",
Expand Down
76 changes: 76 additions & 0 deletions src/praisonai-agents/praisonaiagents/config/feature_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1361,6 +1361,81 @@ class AutonomyLevel(str, Enum):
AUTO_EDIT = "auto_edit"
FULL_AUTO = "full_auto"


@dataclass
class ToolRetryConfig:
"""
Configuration for automatic retry with exponential backoff on tool failures.

Provides structured retry/backoff for transient tool failures like network
timeouts, rate limits, and external service errors. When enabled, retryable
ToolExecutionErrors will automatically retry with configurable exponential
backoff before surfacing to the model.

Usage:
# Simple enable with defaults
agent = Agent(tool_retry_config=ToolRetryConfig())

# Custom configuration
agent = Agent(
tool_retry_config=ToolRetryConfig(
max_attempts=5,
initial_delay_s=2.0,
max_delay_s=60.0,
factor=3.0,
jitter=0.2,
retryable_on=["network", "timeout", "rate_limit"],
)
)

# Disable (default)
agent = Agent(tool_retry_config=None)
"""
# Maximum retry attempts (including initial attempt)
max_attempts: int = 3

# Initial delay in seconds before first retry
initial_delay_s: float = 1.0

# Maximum delay cap in seconds
max_delay_s: float = 30.0

# Exponential backoff factor
factor: float = 2.0

# Random jitter factor (0-1) to avoid thundering herd
jitter: float = 0.1

# Error categories to retry on (maps to ToolExecutionError.error_category)
retryable_on: List[str] = field(default_factory=lambda: ["network", "timeout", "rate_limit"])

def __post_init__(self):
"""Validate configuration parameters."""
if self.max_attempts < 1:
raise ValueError("max_attempts must be at least 1")
if self.initial_delay_s <= 0:
raise ValueError("initial_delay_s must be positive")
if self.max_delay_s <= 0:
raise ValueError("max_delay_s must be positive")
if self.initial_delay_s > self.max_delay_s:
raise ValueError("initial_delay_s cannot be greater than max_delay_s")
if self.factor < 1.0:
raise ValueError("factor must be >= 1.0 for exponential backoff")
if not 0 <= self.jitter <= 1.0:
raise ValueError("jitter must be between 0 and 1")
Comment on lines +1412 to +1425

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate retryable_on contents during initialization.

Line 1410 allows any runtime value shape for retryable_on; invalid entries then fail later in retry filtering instead of failing fast at config parse time.

Suggested patch
 def __post_init__(self):
     """Validate configuration parameters."""
@@
     if not 0 <= self.jitter <= 1.0:
         raise ValueError("jitter must be between 0 and 1")
+    if self.retryable_on is None or any(
+        not isinstance(category, str) or not category.strip()
+        for category in self.retryable_on
+    ):
+        raise ValueError("retryable_on must contain non-empty string categories")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-agents/praisonaiagents/config/feature_configs.py` around lines
1412 - 1425, The __post_init__ method in this configuration class validates
multiple parameters but does not validate the retryable_on field, allowing
invalid entries to pass through and fail later at runtime during retry
filtering. Add validation logic in __post_init__ after the existing parameter
checks to validate the contents of the retryable_on field, ensuring each entry
is of the appropriate type (likely exception classes), and raise a descriptive
ValueError if any invalid entries are found, so configuration errors are caught
immediately at initialization time rather than during runtime retry operations.


def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"max_attempts": self.max_attempts,
"initial_delay_s": self.initial_delay_s,
"max_delay_s": self.max_delay_s,
"factor": self.factor,
"jitter": self.jitter,
"retryable_on": self.retryable_on,
}


# Type aliases for Union types used in Agent.__init__
MemoryParam = Union[bool, MemoryConfig, Any] # Any = MemoryManager instance
KnowledgeParam = Union[bool, List[str], KnowledgeConfig, Any] # Any = KnowledgeBase instance
Expand Down Expand Up @@ -1582,6 +1657,7 @@ def resolve_tools(value: ToolParam) -> Optional[ToolConfig]:
"SkillsConfig",
"AutonomyConfig",
"ToolSearchConfig",
"ToolRetryConfig",
# Config classes (Multi-Agent)
"MultiAgentHooksConfig",
"MultiAgentOutputConfig",
Expand Down
14 changes: 6 additions & 8 deletions src/praisonai-agents/praisonaiagents/hooks/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,18 +180,17 @@ def to_dict(self) -> Dict[str, Any]:
class OnRetryInput(HookInput):
"""Input for OnRetry hooks fired during tool execution retries."""
tool_name: str = ""
attempt: int = 1
attempt: int = 1 # Current attempt number (1-based)
delay_ms: int = 0
error: str = ""
max_attempts: int = 0
error: str = "" # Error message from failed attempt
max_attempts: int = 0 # Total max attempts
error_type: str = "unknown"
# Legacy fields for backward compatibility
retry_count: int = 0
max_retries: int = 3
error_message: str = ""
operation: str = "" # tool_call, llm_request, etc.
delay_seconds: float = 0.0 # Delay before retry
attempt: int = 0 # Current attempt number (0-based)
retry_delay_seconds: float = 0.0 # Calculated delay before next retry
Comment on lines +183 to +193

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve legacy delay key and normalize delay fields in retry events.

Line 204 drops the old delay_seconds payload key, and current call paths can set either delay_ms or retry_delay_seconds, producing inconsistent hook payloads for downstream consumers.

Suggested patch
 class OnRetryInput(HookInput):
@@
     operation: str = ""  # tool_call, llm_request, etc.
     retry_delay_seconds: float = 0.0  # Calculated delay before next retry
+    delay_seconds: float = 0.0  # Legacy alias for backward compatibility
@@
     def to_dict(self) -> Dict[str, Any]:
         base = super().to_dict()
+        normalized_retry_delay_s = self.retry_delay_seconds or (self.delay_ms / 1000.0 if self.delay_ms else 0.0)
+        normalized_delay_ms = self.delay_ms or int(normalized_retry_delay_s * 1000)
         base.update({
             "tool_name": self.tool_name,
             "attempt": self.attempt,
-            "delay_ms": self.delay_ms,
+            "delay_ms": normalized_delay_ms,
             "error": self.error,
             "max_attempts": self.max_attempts,
             "error_type": self.error_type,
-            "retry_delay_seconds": self.retry_delay_seconds,
+            "retry_delay_seconds": normalized_retry_delay_s,
+            "delay_seconds": self.delay_seconds or normalized_retry_delay_s,  # legacy key
             # Legacy fields
             "retry_count": self.retry_count,
             "max_retries": self.max_retries,
             "error_message": self.error_message,
             "operation": self.operation
         })
         return base

Also applies to: 204-209

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-agents/praisonaiagents/hooks/events.py` around lines 183 - 193,
The retry event structure in the RetryEvent class has inconsistent delay field
handling and is dropping the legacy delay_seconds key, breaking backward
compatibility for downstream consumers. In the RetryEvent definition (around
line 183-193), add back the legacy delay_seconds field and ensure it's
consistently populated alongside the newer delay_ms and retry_delay_seconds
fields. Additionally, at the location where the retry event payload is
constructed (line 204-209), normalize the delay values so that all three
delay-related keys (delay_seconds, delay_ms, and retry_delay_seconds) are
included in the event payload, with appropriate unit conversions where needed,
to maintain backward compatibility while supporting current delay field usage.


def to_dict(self) -> Dict[str, Any]:
base = super().to_dict()
Expand All @@ -202,13 +201,12 @@ def to_dict(self) -> Dict[str, Any]:
"error": self.error,
"max_attempts": self.max_attempts,
"error_type": self.error_type,
"retry_delay_seconds": self.retry_delay_seconds,
# Legacy fields
"retry_count": self.retry_count,
"max_retries": self.max_retries,
"error_message": self.error_message,
"operation": self.operation,
"delay_seconds": self.delay_seconds,
"attempt": self.attempt
"operation": self.operation
})
return base

Expand Down
Loading
Loading