Skip to content

Commit 345b198

Browse files
chuenchen309claude
andcommitted
fix(types): distinguish a legitimate None result from never-set in streaming
StreamingOutputBase (base for FlowStreamingOutput/CrewStreamingOutput) initialized self._result to None and treated None as "not set" when raising RuntimeError("No result available"). This misfires whenever the actual result legitimately is None -- e.g. a Flow whose final method returns nothing -- even though streaming completed successfully with no error. The sibling StreamSessionBase class in the same file already solves this correctly with a module-level _MISSING sentinel. Apply the same pattern to StreamingOutputBase, and fix the one other call site that assumed None-means-unset (CrewStreamingOutput.results' fallback to self._result). Added a regression test confirming FlowStreamingOutput.result returns None (rather than raising) when _set_result(None) was called. Confirmed it fails against the pre-fix code and passes after. Ran the full test_streaming.py suite (38 passed), ruff check/format, and mypy (clean). Disclosure: I used an AI coding assistant (Claude) to help identify this bug and draft the fix. I independently traced every other read site of self._result in this file to catch a second place (CrewStreamingOutput .results) that also assumed None-means-unset and would have silently broken had I only patched the reported location. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0e5d0ec commit 345b198

2 files changed

Lines changed: 17 additions & 5 deletions

File tree

lib/crewai/src/crewai/types/streaming.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ def __init__(
342342
async_iterator: AsyncIterator[StreamChunk] | None = None,
343343
) -> None:
344344
"""Initialize streaming output base."""
345-
self._result: T | None = None
345+
self._result: T | object = _MISSING
346346
self._completed: bool = False
347347
self._chunks: list[StreamChunk] = []
348348
self._error: Exception | None = None
@@ -370,9 +370,9 @@ def result(self) -> T:
370370
)
371371
if self._error is not None:
372372
raise self._error
373-
if self._result is None:
373+
if self._result is _MISSING:
374374
raise RuntimeError("No result available")
375-
return self._result
375+
return self._result # type: ignore[return-value]
376376

377377
@property
378378
def is_completed(self) -> bool:
@@ -553,8 +553,8 @@ def results(self) -> list[CrewOutput]:
553553
raise self._error
554554
if self._results is not None:
555555
return self._results
556-
if self._result is not None:
557-
return [self._result]
556+
if self._result is not _MISSING:
557+
return [self._result] # type: ignore[list-item]
558558
raise RuntimeError("No results available")
559559

560560
def _set_results(self, results: list[CrewOutput]) -> None:

lib/crewai/tests/test_streaming.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,18 @@ def simple_gen() -> Generator[StreamChunk, None, None]:
179179
list(streaming)
180180
assert streaming.is_completed is True
181181

182+
def test_result_can_legitimately_be_none(self) -> None:
183+
"""Test that a flow result that is legitimately None (e.g. a final
184+
method with no return value) does not raise, distinguishing 'never
185+
set' from 'set to None'."""
186+
187+
def empty_gen() -> Generator[StreamChunk, None, None]:
188+
yield StreamChunk(content="test")
189+
190+
streaming = FlowStreamingOutput(sync_iterator=empty_gen())
191+
streaming._set_result(None)
192+
assert streaming.result is None
193+
182194

183195
class TestCrewKickoffStreaming:
184196
"""Tests for Crew(stream=True).kickoff() method."""

0 commit comments

Comments
 (0)