Skip to content

Commit 91b5581

Browse files
zzstoatzzclaude
andauthored
fix: allow None results for Optional task types (#1232)
* fix: allow None results for Optional task types Tasks with Optional result types (e.g., `str | None`) were incorrectly raising a ValueError when returning None. This fix checks if None is an allowed type in the union before raising the error. Without this fix, tasks like: ```python @task def maybe_value() -> str | None: return None ``` Would fail validation with "Result is None but result type is not None" even though None is explicitly allowed in the type annotation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * test: add unit test for optional result types Verify that tasks with Optional result types (e.g., str | None) can successfully return None without raising validation errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * test: expand coverage for Optional types (Union and complex) * test: simplify tests, add end-to-end execution --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0f8e957 commit 91b5581

3 files changed

Lines changed: 32 additions & 1 deletion

File tree

src/marvin/tasks/task.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
Sequence,
2121
TypeAlias,
2222
TypeVar,
23+
get_args,
2324
)
2425

2526
from pydantic import TypeAdapter
@@ -452,7 +453,10 @@ def validate_result(self, raw_result: Any) -> T:
452453
else:
453454
raise ValueError("Result type is None but result is not None")
454455
elif raw_result is None:
455-
raise ValueError("Result is None but result type is not None")
456+
# Check if None is allowed in the type (e.g., Optional[str] = str | None)
457+
type_args = get_args(result_type)
458+
if type(None) not in type_args:
459+
raise ValueError("Result is None but result type is not None")
456460

457461
type_adapter = get_type_adapter(result_type)
458462
return type_adapter.validate_python(raw_result)

tests/ai/tasks/test_tasks.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,23 @@ def get_prompt(self) -> str:
1212
prompt = task.get_prompt()
1313
assert prompt == 'Say the word "hello"'
1414
assert task.run() == "hello"
15+
16+
17+
class TestOptionalResults:
18+
async def test_optional_result_returns_none(self):
19+
"""Test that tasks with optional result types can actually return None from AI."""
20+
task = Task(
21+
instructions="return null",
22+
result_type=str | None,
23+
)
24+
result = await task.run_async()
25+
assert result is None
26+
27+
async def test_optional_result_returns_value(self):
28+
"""Test that optional tasks can also return actual values."""
29+
task = Task(
30+
instructions='say "hello"',
31+
result_type=str | None,
32+
)
33+
result = await task.run_async()
34+
assert result == "hello"

tests/basic/tasks/test_tasks.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,13 @@ async def test_task_mark_successful():
258258
assert task.result == "test result"
259259

260260

261+
async def test_task_optional_result_type():
262+
"""Test that tasks with Optional result types can validate None."""
263+
task = Task(instructions="test", result_type=str | None)
264+
validated = task.validate_result(None)
265+
assert validated is None
266+
267+
261268
async def test_task_mark_failed():
262269
"""Test marking task as failed."""
263270
task = Task(instructions="Test failure")

0 commit comments

Comments
 (0)