crewAIInc / crewAIInc/crewAI

[BUG] Tool args_schema is built from raw `param.annotation`, so any tool defined under `from __future__ import annotations` is uncallable (`X is not fully defined`)

Open
#7,623 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
58.8k
Forks
8.5k
Avg merge
1d 15h
Merged PRs (30d)
109

Description

Description

Any tool whose defining module starts with from __future__ import annotations (PEP 563) is constructed without error but is permanently uncallable. The decorator succeeds, then the first call raises:

ValueError: Tool 'wallet' arguments validation failed: `Wallet` is not fully defined; you should define `WalletArgs`, then call `Wallet.model_rebuild()`.

The blast radius is not limited to user-defined models: Optional[str], Literal["a", "b"] and pathlib.Path fail identically, because every annotation in such a module is stored as a string/ForwardRef. Only annotations that happen to resolve in crewai/tools/base_tool.py's own namespace survive. @tool exposes no args_schema escape hatch (its parameters are *args, result_schema, result_as_answer, max_usage_count), so the only workaround is deleting the future import.

Root causelib/crewai/src/crewai/tools/base_tool.py:749, in _make_tool:

annotation = (
    param.annotation if param.annotation != param.empty else Any
)
...
args_schema = create_model(class_name, **fields)   # base_tool.py:758

Under PEP 563 param.annotation is a plain str. It is handed straight to pydantic.create_model, and pydantic resolves the resulting ForwardRef against the calling frame's namespace — crewai/tools/base_tool.py — where the user's names do not exist. The same defect is present at _default_args_schema (base_tool.py:230 and :246), BaseTool._set_args_schema (base_tool.py:483), and from_langchain (base_tool.py:447 and :641).

Steps to Reproduce
  1. Create a module whose first line is from __future__ import annotations.
  2. Decorate a function with @crewai.tools.tool (or subclass BaseTool and annotate a _run parameter) using a type defined in that same module, e.g. class WalletArgs(BaseModel).
  3. The tool object is built without error; wallet.args_schema.model_fields["args"].annotation is ForwardRef('WalletArgs').
  4. Call it — tool.run(args={"amount": 2}) or let an agent call it — and it raises ValueError: ... Wallet is not fully defined .... Rendering the tool into a prompt (model_json_schema() / formatted_description) raises PydanticUserError with the same message.

No network or API keys are required; the failure is purely local schema construction and happens at call time, mid-crew-run.

Expected behavior

A tool defined in a module using from __future__ import annotations should behave exactly like the same tool defined in a module without it: the args schema should resolve the user's annotations and the tool should be callable.

Basis: the repo already implements this correctly in a sibling path. CrewStructuredTool._create_schema_from_function (lib/crewai/src/crewai/tools/structured_tool.py:316-330) resolves each parameter through type_hints = get_type_hints(func) and then annotation = type_hints.get(param_name, Any) before create_model, precisely because typing.get_type_hints evaluates against func.__globals__. Under the identical from __future__ import annotations repro, that path yields {'currency': "<enum 'Currency'>"} and returns the correct result, while every raw-param.annotation path raises. Python's PEP 563 specifies that annotations are stored as strings precisely so that get_type_hints can re-evaluate them against the defining module.

Screenshots/Code snippets

repro_final.py:

from __future__ import annotations
from pydantic import BaseModel

from crewai.tools import tool
from crewai.tools.structured_tool import CrewStructuredTool
from crewai.tools.base_tool import BaseTool


class WalletArgs(BaseModel):
    amount: int


@tool("wallet")
def wallet(args: WalletArgs) -> str:
    """Spend from the wallet."""
    return f"spent {args.amount}"


class SubTool(BaseTool):
    name: str = "subtool"
    description: str = "sub"
    def _run(self, args: WalletArgs) -> str:
        return f"spent {args.amount}"


print("annotation seen by pydantic:", wallet.args_schema.model_fields["args"].annotation)

# sibling implementation (get_type_hints) -- works
def good(amount: int) -> str:
    """Control via the sibling get_type_hints implementation."""
    return f"ok {amount}"

good_tool = CrewStructuredTool.from_function(good, name="good")
print("from_function control ->", good_tool.invoke({"amount": 2}))

for label, call in (
    ("@tool.run", lambda: wallet.run(args={"amount": 2})),
    ("BaseTool subclass.run", lambda: SubTool().run(args={"amount": 2})),
):
    try:
        print(label, "->", call())
    except Exception as e:
        print(label, "-> RAISED", type(e).__name__ + ":", e)

try:
    print("prompt render ->", wallet.args_schema.model_json_schema()["properties"])
except Exception as e:
    print("prompt render -> RAISED", type(e).__name__ + ":", e)

blast_radius.py (same from __future__ import annotations first line):

from __future__ import annotations
from typing import Optional, Literal
from pathlib import Path
from pydantic import BaseModel
from crewai.tools import tool


class Currency(BaseModel):
    code: str


@tool("convert")
def convert(amount: int, currency: Currency) -> str:
    """Convert."""
    return f"{amount} {currency}"


@tool("note")
def note(text: Optional[str] = None) -> str:
    """Note."""
    return str(text)


@tool("pathing")
def pathing(p: Path) -> str:
    """Path."""
    return str(p)


@tool("mode")
def mode(m: Literal["a", "b"]) -> str:
    """Mode."""
    return m


for t in (convert, note, pathing, mode):
    print(t.name, "field-ann:", {k: str(v.annotation) for k, v in t.args_schema.model_fields.items()})
    try:
        t.run()
    except Exception as e:
        print(t.name, "RAISED:", type(e).__name__, str(e).splitlines()[0])
Operating System

Other (specify in additional context) — macOS 26.6.2 (Darwin 25.6.0), arm64.

Python Version

3.12.14

crewAI Version

1.15.22 (editable install of commit 5c33fe4c713aa52df359359ad2a39a1ba4ca83f5)

crewAI Tools Version

Not installed / not involved — the failure is in lib/crewai/src/crewai/tools/base_tool.py.

Virtual Environment

Venv (uv)

Evidence

Verifier 1 — === MY REPRO, WITH from future import annotations ===

convert field-ann: {'amount': "<class 'int'>", 'currency': "ForwardRef('Currency')"}
convert RAISED: ValueError Tool 'convert' arguments validation failed: `Convert` is not fully defined; you should define `Currency`, then call `Convert.model_rebuild()`.
note RAISED: ... `Note` is not fully defined; you should define `Optional`
pathing RAISED: ... should define `Path`
mode RAISED: ... should define `Literal`
=== CONTROL, same file without the future import ===
convert field-ann: {'currency': "<enum 'Currency'>"} / convert RESULT: 5 Currency.USD
note/pathing/mode all RESULT: ok

Verifier 2:

annotation seen by pydantic: ForwardRef('WalletArgs')
from_function control -> ok 2
wallet/@tool: run -> RAISED ValueError: `Wallet` is not fully defined; define `WalletArgs`, then call `Wallet.model_rebuild()`
opt/@tool: run -> RAISED ValueError: `Opt` is not fully defined; ... you should define `Optional`
pth/@tool: run -> RAISED ValueError: `Pth` is not fully defined; ... you should define `pathlib`
render_text_description_and_args -> RAISED PydanticUserError: `Wallet` is not fully defined

Re-run on commit 5c33fe4c713aa52df359359ad2a39a1ba4ca83f5 (crewAI 1.15.22, pydantic 2.12.5, Python 3.12.14, macOS 26.6.2) — repro_final.py:

annotation seen by pydantic: ForwardRef('WalletArgs')
from_function control -> ok 2
@tool.run -> RAISED ValueError: Tool 'wallet' arguments validation failed: `Wallet` is not fully defined; you should define `WalletArgs`, then call `Wallet.model_rebuild()`.
BaseTool subclass.run -> RAISED ValueError: Tool 'subtool' arguments validation failed: `SubToolSchema` is not fully defined; you should define `WalletArgs`, then call `SubToolSchema.model_rebuild()`.
prompt render -> RAISED PydanticUserError: `Wallet` is not fully defined; you should define `WalletArgs`, then call `Wallet.model_rebuild()`.

Re-run of blast_radius.py on the same environment:

convert field-ann: {'amount': "<class 'int'>", 'currency': "ForwardRef('Currency')"}
convert RAISED: ValueError Tool 'convert' arguments validation failed: `Convert` is not fully defined; you should define `Currency`, then call `Convert.model_rebuild()`.
note field-ann: {'text': "ForwardRef('Optional[str]')"}
note RAISED: ValueError Tool 'note' arguments validation failed: `Note` is not fully defined; you should define `Optional`, then call `Note.model_rebuild()`.
pathing field-ann: {'p': "ForwardRef('Path')"}
pathing RAISED: ValueError Tool 'pathing' arguments validation failed: `Pathing` is not fully defined; you should define `Path`, then call `Pathing.model_rebuild()`.
mode field-ann: {'m': 'ForwardRef("Literal[\'a\', \'b\']")'}
mode RAISED: ValueError Tool 'mode' arguments validation failed: `Mode` is not fully defined; you should define `Literal`, then call `Mode.model_rebuild()`.

Control — the exact same four tools in a module without the future import:

convert field-ann: {'amount': "<class 'int'>", 'currency': "<class '__main__.Currency'>"}
convert RESULT: 5 {'code': 'USD'}
note RESULT: hi
pathing RESULT: /tmp
mode RESULT: a
Possible Solution

Resolve annotations before handing them to pydantic, mirroring the sibling implementation: in _make_tool, _default_args_schema, BaseTool._set_args_schema and both from_langchains, compute get_type_hints(target_callable) (target being the decorated f, cls._run, or tool.func) and use hints.get(name, Any) instead of the raw param.annotation. I have a working patch for this and am happy to open a PR — or happy to be assigned if you would rather scope it another way.

Additional context
  • Environment detail for the "Other" OS option: macOS 26.6.2, Apple silicon (Darwin 25.6.0), Python 3.12.14, pydantic 2.12.5, crewAI installed in a uv venv as an editable install of commit 5c33fe4c713aa52df359359ad2a39a1ba4ca83f5.
  • Reachability: any user module using from __future__ import annotations (a common convention — ruff's FA rules, pyupgrade, and this repository's own modules including lib/crewai/src/crewai/tools/base_tool.py all use it) that decorates a function with @crewai.tools.tool or subclasses BaseTool and annotates an argument with a locally defined type, Optional, Literal, or Path.
  • Related issue/PR references found by collision checks: none. Searching this repo for get_type_hints, ForwardRef, PEP 563, from __future__ import annotations and "is not fully defined" returned no existing issue covering this defect.
  • AI disclosure: this issue was authored by an AI agent (Claude Code). .github/CONTRIBUTING.md requires the llm-generated label on AI-authored issues; I attempted to apply it, but the API rejected the request (BlueX888 does not have the correct permissions to execute AddLabelsToLabelable), so a maintainer will need to add it. Disclosing here since I cannot set the label myself.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in lib/crewai/src/crewai/tools/base_tool.py at _make_tool, _default_args_schema, BaseTool._set_args_schema, and the from_langchain paths; compare them with CrewStructuredTool._create_schema_from_function in structured_tool.py. Run repro_final.py and the blast_radius.py examples, then verify that tools using from future import annotations build schemas, render descriptions, and run successfully across these entry points.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.