Skip to content

[BUG] Task rejects valid guardrail return annotations with postponed annotations #7904

Description

@Sinfony8838

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.py exactly matches blob 22ab6aaa49f91e7babaf66dabf200da52ff64207 on current main 738c8e19e35c2888d8e0663bc5cc45c5acf6ac2d.
  • A 57-case regression matrix using the real public Task constructor 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

Activity

  1. divyuuu commented on Oct 6, 2026

    @divyuuu

    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_function applies get_origin/get_args directly to sig.return_annotation, which is the string 'tuple[bool, Any]' under from __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 (via get_type_hints, unwrapping partials 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-generated label where needed.

  2. Sinfony8838 commented on Oct 6, 2026

    @Sinfony8838
    Author

    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.

  3. Sinfony8838 commented on Oct 6, 2026

    @Sinfony8838
    Author

    @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.

  4. divyuuu commented on Oct 7, 2026

    @divyuuu

    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. a TYPE_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.

  5. Sinfony8838 commented on Oct 7, 2026

    @Sinfony8838
    Author

    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.

  6. divyuuu commented on Oct 7, 2026

    @divyuuu

    Done — #7917 now carries the consolidated coverage: callable-object and partial wrappers 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.py next 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.

  7. divyuuu commented on Oct 8, 2026

    @divyuuu

    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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions