Skip to content

fix: #6568 - Replace bare raise with PermanentUploadError in factory.py - #6700

Open
Diwak4r wants to merge 3 commits into
crewAIInc:mainfrom
Diwak4r:fix/6568-bare-raise-factory
Open

fix: #6568 - Replace bare raise with PermanentUploadError in factory.py#6700
Diwak4r wants to merge 3 commits into
crewAIInc:mainfrom
Diwak4r:fix/6568-bare-raise-factory

Conversation

@Diwak4r

@Diwak4r Diwak4r commented Jul 28, 2026

Copy link
Copy Markdown

Summary

Fixes #6568 - Replaces two bare raise statements with explicit PermanentUploadError in get_uploader() factory function.

Changes

  • lib/crewai-files/src/crewai_files/uploaders/factory.py:
    • Line ~200: Bedrock missing bucket config → raise PermanentUploadError(...)
    • Line ~219: Unknown provider → raise PermanentUploadError(...)
    • All ImportError cases now raise PermanentUploadError with helpful install instructions

Root Cause

Bare raise statements outside exception handlers cause RuntimeError: No active exception to re-raise. The package already has PermanentUploadError for non-retryable config/input errors.

Pattern

Same fix pattern as #6430 (bare raise in ToolUsage) for consistency.

Diwak4r added 3 commits July 28, 2026 12:39
…_original_tool_calling for consistent error handling
… executor

- Added async _execute_single_native_tool_call_async method using CrewStructuredTool.ainvoke for proper async tool support
- Changed execute_native_tool to async method using asyncio.gather for parallel execution
- Sync tools still work via fallback to _available_functions in thread pool
- Async tools now run on caller's event loop (MainThread) instead of via asyncio.run() in worker thread
- Updated test_execute_native_tool tests to await the async method
- Matches ReAct executor's async-aware dispatch pattern
… factory.py

- Line 199: Bedrock missing bucket config now raises PermanentUploadError
- Line 216: Unknown provider now raises PermanentUploadError
- All ImportError cases now raise PermanentUploadError with helpful messages
- Matches pattern from crewAIInc#6430 fix for consistent error handling
Copilot AI review requested due to automatic review settings July 28, 2026 10:36
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Native tool execution

Layer / File(s) Summary
Async native-tool batching
lib/crewai/src/crewai/experimental/agent_executor.py
execute_native_tool is now asynchronous, using asyncio.gather for parallel calls and awaited sequential execution when required.
Single-call async semantics and validation
lib/crewai/src/crewai/experimental/agent_executor.py, lib/crewai/src/crewai/tools/tool_usage.py, lib/crewai/tests/agents/test_agent_executor.py
Async single-call execution preserves caching, events, hooks, limits, results, and tests; argument failures now consistently use ToolUsageError.

Uploader error handling

Layer / File(s) Summary
Permanent uploader failure paths
lib/crewai-files/src/crewai_files/uploaders/factory.py
Missing provider dependencies, Bedrock S3 configuration, and unsupported providers now raise PermanentUploadError with specific messages.

Sequence Diagram(s)

sequenceDiagram
  participant AgentExecutor
  participant asyncio
  participant CrewStructuredTool
  participant crewai_event_bus
  AgentExecutor->>asyncio: schedule native tool coroutines
  asyncio->>CrewStructuredTool: invoke tool asynchronously
  CrewStructuredTool-->>AgentExecutor: return tool result metadata
  AgentExecutor->>crewai_event_bus: emit tool usage events
Loading

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also changes async native tool execution and tool-usage error handling, which are unrelated to #6568's factory.py fix. Split the agent_executor.py and tool_usage.py changes into a separate PR, or document why they are required for #6568 if they must stay together.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main factory change: replacing bare raises with PermanentUploadError for #6568.
Description check ✅ Passed The description matches the implemented factory.py fix and its error-handling rationale.
Linked Issues check ✅ Passed The factory.py changes replace the two bare raises in get_uploader() with PermanentUploadError as required by #6568.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/crewai/tests/agents/test_agent_executor.py (1)

1268-1273: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Tighten the parallelism timing bound.

The test uses two 0.2s synchronous callables under the _available_functions fallback, which can still complete in ~0.4s sequentially and pass elapsed < 0.5. Change the assertion to a value below the sequential total, such as elapsed < 0.35, so loss of concurrency is caught. asyncio is already imported at module scope.

🤖 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 `@lib/crewai/tests/agents/test_agent_executor.py` around lines 1268 - 1273,
Update the elapsed-time assertion in the native tool execution test around
executor.execute_native_tool() from the current 0.5-second bound to a threshold
below the 0.4-second sequential runtime, such as 0.35 seconds, so the test
detects lost concurrency while preserving the existing result assertion.

Source: Coding guidelines

🧹 Nitpick comments (3)
lib/crewai-files/src/crewai_files/uploaders/factory.py (2)

218-219: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align the docstring with the new exception behavior.

Line 138 still documents None for unsupported providers, but this branch now raises PermanentUploadError. Update the Returns/Raises sections to reflect the actual contract.

Suggested docstring update
     Returns:
-        FileUploader instance for the provider, or None if not supported.
+        FileUploader instance for the provider.
+
+    Raises:
+        PermanentUploadError: If the provider is unsupported or unavailable.

As per coding guidelines, public APIs and complex logic in Python code should be documented.

🤖 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 `@lib/crewai-files/src/crewai_files/uploaders/factory.py` around lines 218 -
219, Update the docstring for the uploader factory method containing the
unsupported-provider branch to remove the documented None return for unsupported
providers and state that it raises PermanentUploadError instead. Keep the
documented return behavior for supported providers accurate, and ensure the
Raises section names this exception and condition.

Source: Coding guidelines


154-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression tests for the new permanent-error paths.

Add behavior-focused tests asserting PermanentUploadError and its actionable message for missing provider dependencies, missing Bedrock configuration, and unsupported providers. This should also guard against regression to RuntimeError: No active exception to re-raise.

As per coding guidelines, **/tests/**/*.py should write unit tests for new functionality that focus on behavior rather than implementation details. The PR objective also explicitly requires regression tests.

Also applies to: 169-169, 187-187, 200-202, 216-216, 218-219

🤖 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 `@lib/crewai-files/src/crewai_files/uploaders/factory.py` at line 154, Add
behavior-focused unit tests covering each new permanent-error path in the
uploader factory, including missing provider dependencies, missing Bedrock
configuration, and unsupported providers. Assert that PermanentUploadError is
raised with actionable messages, and verify none of these paths produces “No
active exception to re-raise”; place the tests under the project’s tests
directory using existing test conventions.

Source: Coding guidelines

lib/crewai/src/crewai/tools/tool_usage.py (1)

846-854: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Preserve the original parse error when converting to ToolUsageError.

_validate_tool_input raises specific, actionable messages (e.g. "Tool input must be a valid dictionary in JSON or Python literal format"). Collapsing every failure into the generic tool_arguments_error string loses that detail both from the traceback and from what the agent sees on the retry. Chaining with from e also satisfies Ruff's B904.

The four repeated f"{I18N_DEFAULT.errors('tool_arguments_error')}" expressions can be hoisted into one local; the f-string wrapper is a no-op since errors() already returns a string.

♻️ Proposed refactor
-        except Exception:
+        except Exception as e:
+            arguments_error = I18N_DEFAULT.errors("tool_arguments_error")
             if raise_error:
-                raise ToolUsageError(f"{I18N_DEFAULT.errors('tool_arguments_error')}")
-            return ToolUsageError(f"{I18N_DEFAULT.errors('tool_arguments_error')}")
+                raise ToolUsageError(f"{arguments_error}: {e}") from e
+            return ToolUsageError(f"{arguments_error}: {e}")
 
         if not isinstance(arguments, dict):
+            arguments_error = I18N_DEFAULT.errors("tool_arguments_error")
             if raise_error:
-                raise ToolUsageError(f"{I18N_DEFAULT.errors('tool_arguments_error')}")
-            return ToolUsageError(f"{I18N_DEFAULT.errors('tool_arguments_error')}")
+                raise ToolUsageError(arguments_error)
+            return ToolUsageError(arguments_error)
🤖 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 `@lib/crewai/src/crewai/tools/tool_usage.py` around lines 846 - 854, Update the
exception handling in _validate_tool_input to preserve the original parse
exception when raising or returning ToolUsageError: capture the exception as e,
chain raised errors with “from e”, and include the original error message in the
ToolUsageError presented to the agent instead of replacing it with only the
generic tool_arguments_error text. Hoist the repeated
I18N_DEFAULT.errors('tool_arguments_error') result into one local variable and
remove the unnecessary f-string wrappers.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@lib/crewai/src/crewai/experimental/agent_executor.py`:
- Line 1743: Update the asyncio.gather call in the parallel native tool
execution flow to use return_exceptions=True, matching execute_todos_parallel.
Preserve all completed sibling results and ensure any gathered exception is
converted into the existing tool-result format before constructing the assistant
message, so every tool call still receives a matching tool message.
- Around line 2051-2058: The unused synchronous native-tool helper should be
removed, leaving _execute_single_native_tool_call_async as the sole native tool
execution implementation. Extract any shared context setup and result-handling
logic into reusable helpers, then update the async method to use them while
preserving its current tool-call resolution and await behavior.
- Around line 1737-1743: Update the async native tool execution flow around
_execute_single_native_tool_call_async so synchronous tool fallbacks, including
direct tool_func calls and CrewStructuredTool.ainvoke wrapping sync callables,
are offloaded with asyncio.to_thread rather than running on the event loop.
Preserve direct awaiting for genuinely asynchronous tools so asyncio.gather
provides real concurrency for both paths.

---

Outside diff comments:
In `@lib/crewai/tests/agents/test_agent_executor.py`:
- Around line 1268-1273: Update the elapsed-time assertion in the native tool
execution test around executor.execute_native_tool() from the current 0.5-second
bound to a threshold below the 0.4-second sequential runtime, such as 0.35
seconds, so the test detects lost concurrency while preserving the existing
result assertion.

---

Nitpick comments:
In `@lib/crewai-files/src/crewai_files/uploaders/factory.py`:
- Around line 218-219: Update the docstring for the uploader factory method
containing the unsupported-provider branch to remove the documented None return
for unsupported providers and state that it raises PermanentUploadError instead.
Keep the documented return behavior for supported providers accurate, and ensure
the Raises section names this exception and condition.
- Line 154: Add behavior-focused unit tests covering each new permanent-error
path in the uploader factory, including missing provider dependencies, missing
Bedrock configuration, and unsupported providers. Assert that
PermanentUploadError is raised with actionable messages, and verify none of
these paths produces “No active exception to re-raise”; place the tests under
the project’s tests directory using existing test conventions.

In `@lib/crewai/src/crewai/tools/tool_usage.py`:
- Around line 846-854: Update the exception handling in _validate_tool_input to
preserve the original parse exception when raising or returning ToolUsageError:
capture the exception as e, chain raised errors with “from e”, and include the
original error message in the ToolUsageError presented to the agent instead of
replacing it with only the generic tool_arguments_error text. Hoist the repeated
I18N_DEFAULT.errors('tool_arguments_error') result into one local variable and
remove the unnecessary f-string wrappers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 630850a9-0ef9-46ae-966d-0edaaafbea99

📥 Commits

Reviewing files that changed from the base of the PR and between 2e95bfb and 70c96f4.

📒 Files selected for processing (4)
  • lib/crewai-files/src/crewai_files/uploaders/factory.py
  • lib/crewai/src/crewai/experimental/agent_executor.py
  • lib/crewai/src/crewai/tools/tool_usage.py
  • lib/crewai/tests/agents/test_agent_executor.py

Comment on lines 1737 to +1743
if should_parallelize:
max_workers = min(8, len(runnable_tool_calls))
with ThreadPoolExecutor(max_workers=max_workers) as pool:
future_to_idx = {
pool.submit(
contextvars.copy_context().run,
self._execute_single_native_tool_call,
tool_call,
): idx
for idx, tool_call in enumerate(runnable_tool_calls)
}
ordered_results: list[dict[str, Any] | None] = [None] * len(
runnable_tool_calls
)
for future in as_completed(future_to_idx):
idx = future_to_idx[future]
try:
ordered_results[idx] = future.result()
except Exception as e:
tool_call = runnable_tool_calls[idx]
info = extract_tool_call_info(tool_call)
call_id = info[0] if info else "unknown"
func_name = info[1] if info else "unknown"
ordered_results[idx] = {
"call_id": call_id,
"func_name": func_name,
"result": f"Error executing tool: {e}",
"from_cache": False,
"original_tool": None,
}
execution_results = [
result for result in ordered_results if result is not None
]
# Execute in parallel using async version for proper async tool support
tasks = [
self._execute_single_native_tool_call_async(tool_call)
for tool_call in runnable_tool_calls
]
execution_results = await asyncio.gather(*tasks)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Parallel branch no longer runs sync tools concurrently.

The previous ThreadPoolExecutor path gave real parallelism for sync tools. asyncio.gather over coroutines that call tool_func(**args_dict) (Line 2207) or a CrewStructuredTool.ainvoke that wraps a sync callable executes them inline on the event loop, so sync tools serialize and block the loop (delaying event emission, callbacks, and any other concurrent flow work). Only genuinely async tools benefit.

Consider offloading the sync fallback via asyncio.to_thread — the same pattern already used in execute_todos_parallel (Line 1231).

Note the existing test (test_execute_native_tool_runs_parallel_for_multiple_calls, bound elapsed < 0.5 for two 0.2s tools) passes even under fully sequential execution, so it won't catch this.

♻️ Offload the sync fallback to a thread
-                    tool_func = self._available_functions[func_name]
-                    raw_result = tool_func(**args_dict)
+                    tool_func = self._available_functions[func_name]
+                    raw_result = await asyncio.to_thread(
+                        functools.partial(tool_func, **args_dict)
+                    )
🤖 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 `@lib/crewai/src/crewai/experimental/agent_executor.py` around lines 1737 -
1743, Update the async native tool execution flow around
_execute_single_native_tool_call_async so synchronous tool fallbacks, including
direct tool_func calls and CrewStructuredTool.ainvoke wrapping sync callables,
are offloaded with asyncio.to_thread rather than running on the event loop.
Preserve direct awaiting for genuinely asynchronous tools so asyncio.gather
provides real concurrency for both paths.

self._execute_single_native_tool_call_async(tool_call)
for tool_call in runnable_tool_calls
]
execution_results = await asyncio.gather(*tasks)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

asyncio.gather without return_exceptions=True discards sibling results.

_execute_single_native_tool_call_async catches most tool errors, but anything raised outside those try blocks (e.g. a hook raising in run_before_tool_call_hooks, or format_native_tool_output_for_agent) propagates out of gather and drops the already-completed tool results, leaving the assistant message with tool_calls but no matching tool messages — which most providers reject on the next turn. execute_todos_parallel (Line 1240) already guards with return_exceptions=True.

🤖 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 `@lib/crewai/src/crewai/experimental/agent_executor.py` at line 1743, Update
the asyncio.gather call in the parallel native tool execution flow to use
return_exceptions=True, matching execute_todos_parallel. Preserve all completed
sibling results and ensure any gathered exception is converted into the existing
tool-result format before constructing the assistant message, so every tool call
still receives a matching tool message.

Comment on lines +2051 to +2058
async def _execute_single_native_tool_call_async(
self, tool_call: Any
) -> dict[str, Any]:
"""Execute a single native tool call asynchronously using ainvoke.

This method properly awaits async tools on the current event loop,
matching the behavior of the ReAct executor's CrewStructuredTool.ainvoke.
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=py '_execute_single_native_tool_call\b(?!_async)' -C2

Repository: crewAIInc/crewAI

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="lib/crewai/src/crewai/experimental/agent_executor.py"

echo "== file existence and outline matches =="
wc -l "$file"
rg -n "def _execute_single_native_tool_call(_async)?|class .*|async def .*_|def .*native|_available_functions|ainvoke" "$file" | sed -n '1,220p'

echo
echo "== sync version excerpt =="
sed -n '1800,2080p' "$file" | nl -ba -v1800

echo
echo "== async version excerpt =="
sed -n '2020,2270p' "$file" | nl -ba -v2020

echo
echo "== exact call sites in repository (context) =="
rg -n -C3 "_execute_single_native_tool_call(_async)?\b" --type=py .

Repository: crewAIInc/crewAI

Length of output: 2102


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="lib/crewai/src/crewai/experimental/agent_executor.py"

python3 - <<'PY' "$file"
# Read-only static and semantic comparison of the two native-tool-call helper bodies.
# Uses only the file contents; it does not execute repository code.
import ast
import sys
from pathlib import Path

file = sys.argv[1]
source = Path(file).read_text(encoding="utf-8")
tree = ast.parse(source)

defs = {node.name: node for node in ast.walk(tree) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))}
target = ["_execute_single_native_tool_call", "_execute_single_native_tool_call_async"]
for name in target:
    node = defs.get(name)
    print(f"== {name} ==")
    if not node:
        print("NOT FOUND")
        continue
    start = node.body[0].lineno if node.body else "N/A"
    end = node.body[-1].lineno if node.body else "N/A"
    print(f"line {node.lineno}-{node.end_lineno}, async={isinstance(node, ast.AsyncFunctionDef)}, body start line {start}, end line {end}")
    lines = source.splitlines()
    for i in range(node.lineno, node.end_lineno + 1):
        t = lines[i-1].rstrip()
        stripped = t.strip()
        if stripped.startswith(("await ", "async for ",)):
            print(f"  {i}: {t}")
        elif stripped == "return " or stripped.startswith("return "):
            # Print until line break if multi-line ast source not used; here line is enough.
            print(f"  {i}: {t}")

sync_1859 = "\n".join(source.splitlines()[1859-1:1936])
async_2051 = "\n".join(source.splitlines()[2051-1:2128])
print("\n== head similarity ==")
print("first_line_both_equal=", sync_1859.strip().splitlines()[0] == async_2051.strip().splitlines()[0])
print("sync_len=", len(sync_1859.splitlines()), "async_len=", len(async_2051.splitlines()))

# Also search call sites via plain text/source-level search as rg is unavailable.
print("\n== call-site scan ==")
for i, line in enumerate(source.splitlines(), 1):
    if "_execute_single_native_tool_call(" in line or "_execute_single_native_tool_call_async(" in line:
        print(f"{i}: {line.rstrip()}")
PY

echo
echo "== relevant excerpts =="
sed -n '1859,2050p' "$file"
echo
sed -n '2051,2249p' "$file"

Repository: crewAIInc/crewAI

Length of output: 17101


Remove the unused sync native-tool helper

_execute_single_native_tool_call is not referenced by any call site; the native tool execution path now resolves tool calls and awaits _execute_single_native_tool_call_async. Keeping the sync helper only adds ~190 lines of duplicated pre/post execution logic. Extract the shared context/result handling helpers and have the async method be the single implementation.

🤖 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 `@lib/crewai/src/crewai/experimental/agent_executor.py` around lines 2051 -
2058, The unused synchronous native-tool helper should be removed, leaving
_execute_single_native_tool_call_async as the sole native tool execution
implementation. Extract any shared context setup and result-handling logic into
reusable helpers, then update the async method to use them while preserving its
current tool-call resolution and await behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Bare raise in get_uploader() causes RuntimeError instead of PermanentUploadError

2 participants