Repository navigation
[BUG] Task rejects valid guardrail return annotations with postponed annotations #7904
Description
Description
Task construction rejects a valid callable guardrail return annotation when its module uses from __future__ import annotations. The identical eager annotation is accepted. This occurs before agent execution and needs no model credentials or network access.
The validator applies get_origin/get_args directly to inspect.signature(v).return_annotation. With postponed annotations, that value is a string.
Steps to Reproduce
Run the following with CrewAI installed. Remove the future import to see the same function accepted.
Screenshots/Code snippets
from __future__ import annotations
from typing import Any
from crewai import Task
def guardrail(output) -> tuple[bool, Any]:
return True, output
Task(description="Describe one item", expected_output="A string", guardrail=guardrail)Expected behavior
A valid return annotation should be accepted in both annotation modes. The guardrail documentation recommends optional type hints and specifies the tuple return contract.
Operating System
Other: Debian GNU/Linux 13 (Linux)
Python Version
3.12.14
crewAI Version
1.15.23
crewAI Tools Version
Not installed; this reproduction only needs the core package.
Virtual Environment
Venv (created with uv)
Evidence
pydantic_core.ValidationError: 1 validation error for Task
guardrail
Value error, If return type is annotated, it must be Tuple[bool, Any]
- Pydantic 2.12.5. The installed
task.pyexactly matches blob22ab6aaa49f91e7babaf66dabf200da52ff64207on current main738c8e19e35c2888d8e0663bc5cc45c5acf6ac2d. - A 57-case regression matrix using the real public
Taskconstructor gives 18 failures and 39 passing controls on unchanged code. - The local fix passes those 57 cases, 30 independent compatibility cases, and 19 existing offline guardrail tests: 106 passed, 3 LLM/VCR-dependent tests excluded. Outbound sockets were disabled.
- Coverage includes functions, callable objects, decorators, partials, eager/postponed/quoted annotations, untouched input annotations, invalid return shapes/signatures, and unresolved annotations.
- This is focused verification, not a complete repository-suite, full mypy, or Python-version-matrix pass.
Possible Solution
Resolve only postponed return annotations in the defining callable's namespace before applying the existing structural checks. Preserve annotation metadata and leave input annotations untouched. Retain rejection when a return hint cannot be resolved.
Additional context
- Validator source.
- Historical [FEATURE]More comfortable guardrail validation #2177/Fix: More comfortable validation #2177 #2178 concerns concrete tuple and stricter return forms. This report concerns postponed annotations.
- Related [BUG] Tool args_schema is built from raw
param.annotation, so any tool defined underfrom __future__ import annotationsis uncallable (X is not fully defined) #7623/fix(tools): resolve tool argument annotations in the caller's module #7626 concerns tool argument schemas inbase_tool.py, not Task guardrail return validation. - AI disclosure: this report and proposed fix were prepared with AI assistance. A Kimi audit suggested the hypothesis, which was independently reproduced and tested. The required
llm-generatedlabel is requested; please apply it if this account cannot set labels.
Activity
divyuuu commentedon Oct 6, 2026
Hi, I’d like to pick up #7904 if it’s still available — is anyone already working on it?
I’ve reproduced it locally:
validate_guardrail_functionappliesget_origin/get_argsdirectly tosig.return_annotation, which is the string'tuple[bool, Any]'underfrom __future__ import annotations, so a valid guardrail is rejected before agent execution. The fix I’m preparing resolves only the postponed return annotation in the callable’s own namespace (viaget_type_hints, unwrappingpartials and falling back to__call__for callable objects) before the existing structural checks; unresolvable hints stay rejected and input annotations are untouched. I’ll include regression tests showing a valid postponed guardrail is accepted while invalid shapes and unresolvable names are still rejected.If that sounds good, I’ll open a focused PR referencing this issue. I’m a first-time contributor and used claude Opus for assistance with the investigation — maintainers, please apply the
llm-generatedlabel where needed.Sinfony8838 commentedon Oct 6, 2026
Thanks for checking, @divyuuu! I've already opened draft PR #7905 with a fix for postponed return annotations and regression tests. It leaves input annotations untouched and preserves rejection of invalid or unresolved return hints. Please take a look and feel free to review or flag any gaps so we can avoid duplicating the work.
Sinfony8838 commentedon Oct 6, 2026
@divyuuu, I’ve now seen #7917. One edge case worth sharing: get_type_hints(target) also evaluates input annotations, so an unresolved input hint can cause a valid postponed return hint to be rejected. Draft #7905 uses a return-only helper with the callable’s globals and include_extras=True. These regression tests cover unavailable input types and ensure input expressions aren’t evaluated. Feel free to reuse the approach and tests so we can coordinate on a common fix.
divyuuu commentedon Oct 7, 2026
Thanks for flagging this, @Sinfony8838 — you’re right: my first version called
get_type_hints(target), which evaluates every annotation, so an unresolvable input hint (e.g. aTYPE_CHECKING-only import) would still reject a valid postponed return.I’ve updated #7917 to resolve only the return hint, reusing the approach you shared (a dummy function carrying just the return annotation, resolved with the callable’s globals and
include_extras=True), plus a regression test pairing an unresolvable postponed input annotation with a valid postponed return. Credit to your draft #7905 for the pattern — happy to converge on whichever version maintainers prefer, and glad to review yours.Sinfony8838 commentedon Oct 7, 2026
Thanks, @divyuuu, for adopting and crediting the return-only pattern and for reviewing #7905. Agreed on one consolidated fix in whichever PR the maintainers prefer; there’s no need to maintain duplicate implementations. Could we preserve #7905’s broader regression coverage and attribution in the consolidated version, especially input-expression non-evaluation, annotation immutability, quoted hints, and callable wrappers? This issue and both current fixes are scoped to Task. The LiteAgent gap noted in review could be a separate follow-up if maintainers agree.
divyuuu commentedon Oct 7, 2026
Done — #7917 now carries the consolidated coverage: callable-object and
partialwrappers with postponed annotations, input-expression non-evaluation, annotation immutability, and explicitly quoted return hints (eager + postponed), each verified against the return-only resolution. The test section credits your #7905 matrix as its source.I kept the tests in
test_task.pynext to the existing guardrail validation tests rather than a new file, to keep the diff minimal — happy to restructure if maintainers prefer your standalone-file layout. Over to maintainer direction on which PR to take.divyuuu commentedon Oct 8, 2026
One more offer while #7917 awaits review: you mentioned the LiteAgent gap as a possible separate follow-up if maintainers agree. If they bless that scope, I’d be glad to take it on — it’s a natural extension of the return-only work, and I’d bring the same regression discipline. Maintainers, just point me at the expected behavior and I’ll open a focused PR for it.