[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`)
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 cause — lib/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
- Create a module whose first line is
from __future__ import annotations. - Decorate a function with
@crewai.tools.tool(or subclassBaseTooland annotate a_runparameter) using a type defined in that same module, e.g.class WalletArgs(BaseModel). - The tool object is built without error;
wallet.args_schema.model_fields["args"].annotationisForwardRef('WalletArgs'). - Call it —
tool.run(args={"amount": 2})or let an agent call it — and it raisesValueError: ...Walletis not fully defined .... Rendering the tool into a prompt (model_json_schema()/formatted_description) raisesPydanticUserErrorwith 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
uvvenv as an editable install of commit5c33fe4c713aa52df359359ad2a39a1ba4ca83f5. - Reachability: any user module using
from __future__ import annotations(a common convention — ruff'sFArules,pyupgrade, and this repository's own modules includinglib/crewai/src/crewai/tools/base_tool.pyall use it) that decorates a function with@crewai.tools.toolor subclassesBaseTooland annotates an argument with a locally defined type,Optional,Literal, orPath. - Related issue/PR references found by collision checks: none. Searching this repo for
get_type_hints,ForwardRef, PEP 563,from __future__ import annotationsand"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.mdrequires thellm-generatedlabel 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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