-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fix: Agent runtime lacks structured retry/backoff for tool failures #2080
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f7da753
5f699c0
6db68e5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| 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 | ||
| ): | ||
|
|
@@ -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. | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Preserve
💡 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 |
||
|
|
||
| # Process tool_config and artifact storage (moved from tool_output) | ||
| self._artifact_store = None | ||
|
|
||
Large diffs are not rendered by default.
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
fiRepository: 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 -A2Repository: MervinPraison/PraisonAI Length of output: 894 Use identity comparison for Line 107 uses 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 (E721) 🤖 Prompt for AI AgentsSource: 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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-headingRepository: 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.pyRepository: 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.pyRepository: 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.
🧰 Tools🪛 Ruff (0.15.18)[error] 107-107: Use (E721) [warning] 130-130: Do not catch blind exception: (BLE001) 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Validate Line 1410 allows any runtime value shape for 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 |
||
|
|
||
| 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 | ||
|
|
@@ -1582,6 +1657,7 @@ def resolve_tools(value: ToolParam) -> Optional[ToolConfig]: | |
| "SkillsConfig", | ||
| "AutonomyConfig", | ||
| "ToolSearchConfig", | ||
| "ToolRetryConfig", | ||
| # Config classes (Multi-Agent) | ||
| "MultiAgentHooksConfig", | ||
| "MultiAgentOutputConfig", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 baseAlso applies to: 204-209 🤖 Prompt for AI Agents |
||
|
|
||
| def to_dict(self) -> Dict[str, Any]: | ||
| base = super().to_dict() | ||
|
|
@@ -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 | ||
|
|
||
|
|
||
There was a problem hiding this comment.
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.pyRepository: MervinPraison/PraisonAI
Length of output: 218
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 1164
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 2010
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 1607
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 1702
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 449
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 213
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 210
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 1056
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 4090
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 922
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 224
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 2859
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 183
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 323
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 161
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 786
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 1780
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 7203
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 981
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 161
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 161
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 1596
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 1098
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 411
Add
tool_retry_configto clone propagation and align type annotation/documentation with runtime behavior.The
tool_retry_configparameter acceptsNone,False,True,dict, andToolRetryConfiginstance at runtime (lines 1526–1540), but the type annotation (line 600) and docstring (line 717–720) only describeToolRetryConfiginstance. Additionally,clone_for_channel()omitstool_retry_configfrom the cloned configuration, causing state loss during agent cloning.💡 Required fixes
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 configurationclone_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
Source: Linters/SAST tools