Skip to content

[BUG] TUI crashes with TypeError: Object of type ellipsis is not JSON serializable when streamed output contains a literal [...] #7434

Description

@dadamonis

Description

Environment

  • crewai 1.15.21 (CLI and library, from PyPI), Python 3.12.10, macOS Tahoe.

Summary

Running a crew via crewai run crashes the live TUI and cancels the in-progress run whenever an agent's streamed text output happens to contain a literal [...] (three dots inside square brackets). The crew's actual execution (LLM calls, tool calls) is unaffected; only the terminal renderer crashes, taking the whole session down with it.

Root cause

In crewai_cli/crew_run_tui.py, _format_json_in_text() scans streamed text for bracketed spans and passes each candidate to _try_parse_structured(). That function tries json.loads() first, then falls back to ast.literal_eval() for non-JSON text, accepting the result as long as it's a dict or list — without checking that its contents are JSON-serializable. ast.literal_eval("[...]") is valid Python and returns [Ellipsis] (bare ... is the Ellipsis singleton), which passes the isinstance(obj, (dict, list)) check. _format_json_in_text then calls json.dumps([Ellipsis], ...), which raises TypeError: Object of type ellipsis is not JSON serializable. This propagates up through _render_main_content → _tick and crashes the whole CrewRunApp, cancelling the run.

Steps to Reproduce

Minimal, deterministic repro (no LLM needed):

from crewai_cli.crew_run_tui import _format_json_in_text
_format_json_in_text("pandas,[...]")
# TypeError: Object of type ellipsis is not JSON serializable

Full CLI repro:

  1. Create any minimal crewai crew (one agent, one task).
  2. Give the task a description that reliably makes the model emit a literal [...], e.g.: "Describe an example Python list of a few package names followed by an ellipsis, formatted exactly like [numpy, pandas, ...]. Include that literal text in your response."
  3. Run crewai run.
  4. As the response streams into the TUI, _format_json_in_text hits the [...] substring, parses it to [Ellipsis], json.dumps raises, and the run is cancelled with Run cancelled.

Expected behavior

Expected my my crew to complete it's coding run with out failure.

Screenshots/Code snippets

from crewai_cli.crew_run_tui import _format_json_in_text
_format_json_in_text("pandas,[...]")
# TypeError: Object of type ellipsis is not JSON serializable

Operating System

Other (specify in additional context)

Python Version

3.12

crewAI Version

1.15.21

crewAI Tools Version

1.15.21

Virtual Environment

Venv

Evidence

╭───────────────────────────────────────────────────────────── Traceback (most recent call last) ──────────────────────────────────────────────────────────
│ /Users/duke/.local/share/uv/tools/crewai/lib/python3.12/site-packages/crewai_cli/crew_run_tui.py:1198 in _tick

│ 1195 │ │ │ with self._lock:
│ 1196 │ │ │ │ self._render_sidebar()
│ 1197 │ │ │ │ self._render_task_header()
│ ❱ 1198 │ │ │ │ self._render_main_content()
│ 1199 │ │ │ │ if self.query_one("#log-panel").display:
│ 1200 │ │ │ │ │ self._render_log_panel()
│ 1201 │ │ except NoMatches:

│ ╭──────────────────────────────────────────────────── locals ────────────────────────────────────────────────────╮
│ │ elapsed = 116.62594890594482 │
│ │ mins = 1 │
│ │ secs = 56 │
│ │ self = CrewRunApp(title='CrewAI — echo_eng_team', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'}) │
│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

│ /Users/duke/.local/share/uv/tools/crewai/lib/python3.12/site-packages/crewai_cli/crew_run_tui.py:1532 in _render_main_content

│ 1529 │ │
│ 1530 │ │ # Streaming output
│ 1531 │ │ if self._is_streaming and self._streaming_text:
│ ❱ 1532 │ │ │ text = self._filtered_streaming_text()
│ 1533 │ │ │ text = _unescape_text(text)
│ 1534 │ │ │ if text.strip():
│ 1535 │ │ │ │ lines = text.rstrip().split("\n")

│ ╭──────────────────────────────────────────────────────────────────────── locals ────────────────────────────────────────────────────────────────────────
│ │ _detail = ''
│ │ completed = False
│ │ desc = 'Compose the final migration plan document synthesizing findings from steps 1-8 a'+482
│ │ msg = 'Generating response…'
│ │ plan_title = "Investigate the echo project's current structure and dependency/packaging setup,"+198
│ │ self = CrewRunApp(title='CrewAI — echo_eng_team', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'})
│ │ short = 'Compose the final migration plan document synthesizing findings from steps 1-8 a'+11
│ │ should_scroll = False
│ │ sn = 9
│ │ st = 'pending'
│ │ step = {
│ │ │ 'step_number': 9,
│ │ │ 'description': 'Compose the final migration plan document synthesizing findings from steps 1-8 a'+482,
│ │ │ 'tool_to_use': 'file_writer_tool',
│ │ │ 'depends_on': [1, 2, 3, 4, 5, 6, 7, 8]
│ │ }
│ │ sty = 'yellow'
│ │ t = <text " PLAN\n ▸ Investigate the echo project's current structure and dependency/packaging setup,\n\n ✔ 1. List the files at the
│ │ root of the echo project directory to understand its layout. DONE wh…\n ✔ 2. Search the internet for the official UV documentation on
│ │ using UV for Python project/devel…\n ✔ 3. Search the internet for official documentation on UV Tools (uv tool install, uv tool
│ │ run/u…\n ⠸ 4. Based on step 1's listing, identify and list the contents of the source/package subdirecto…\n ⠸ 5. Read the current
│ │ Python packaging/config file(s) found in step 1 (e.g., pyproject.toml, se…\n ⠸ 6. Read the current dependency declaration file(s)
│ │ found in step 1 (e.g., requirements.txt, r…\n ○ 7. Read the project entry-point/CLI module identified in step 4 to determine console
│ │ script d…\n ⠸ 8. Read any existing developer/CI/build files found in step 1 (e.g., README, Makefile, tox.in…\n ○ 9. Compose the final
│ │ migration plan document synthesizing findings from steps 1-8 and researc…\n\n ⠸ Generating response…\n\n" [Span(0, 7, 'bold #666666'),
│ │ Span(7, 11, 'bold #1F7982'), Span(11, 92, 'bold #1F7982'), Span(93, 97, '#4aba6a'), Span(97, 192, '#666666'), Span(192, 196,
│ │ '#4aba6a'), Span(196, 291, '#666666'), Span(291, 295, '#4aba6a'), Span(295, 390, '#666666'), Span(390, 394, '#FF5A50'), Span(394, 489,
│ │ '#e0e0e0'), Span(489, 493, '#FF5A50'), Span(493, 588, '#e0e0e0'), Span(588, 592, '#FF5A50'), Span(592, 687, '#e0e0e0'), Span(687, 691,
│ │ '#666666'), Span(691, 786, '#666666'), Span(786, 790, '#FF5A50'), Span(790, 885, '#e0e0e0'), Span(885, 889, '#666666'), Span(889, 984,
│ │ '#666666'), Span(985, 989, '#FF5A50'), Span(989, 1011, '#AAAAAA')] ''>
│ │ widget = Static(id='main-content')
│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

│ /Users/duke/.local/share/uv/tools/crewai/lib/python3.12/site-packages/crewai_cli/crew_run_tui.py:1708 in _filtered_streaming_text

│ 1705 │ │ │ │ text = text[:plan_start].strip()
│ 1706 │ │
│ 1707 │ │ text = self._strip_step_observation_json(text)
│ ❱ 1708 │ │ return _format_json_in_text(text)
│ 1709 │
│ 1710 │ def _strip_step_observation_json(self, text: str) -> str:
│ 1711 │ │ """Hide structured step-observation JSON from the live transcript."""

│ ╭───────────────────────────────────────────────────── locals ──────────────────────────────────────────────────────╮
│ │ plan_start = -1 │
│ │ self = CrewRunApp(title='CrewAI — echo_eng_team', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'}) │
│ │ text = 'requires = ["hat≥3.1.43, pchling"], buildandas≥2.2.-backend = "hatch2, numpy≥'+1370 │ │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │ │ /Users/duke/.local/share/uv/tools/crewai/lib/python3.12/site-packages/crewai_cli/crew_run_tui.py:122 in _format_json_in_text │ │ 119 │ │ │ │ │ │ candidate = text[i : j + 1] │ 120 │ │ │ │ │ │ parsed = _try_parse_structured(candidate) │ 121 │ │ │ │ │ │ if parsed is not None: │ ❱ 122 │ │ │ │ │ │ │ formatted = _json.dumps( │ 123 │ │ │ │ │ │ │ │ parsed, indent=2, ensure_ascii=False │ 124 │ │ │ │ │ │ │ ) │ 125 │ │ │ │ │ │ │ result.append(formatted) │ │ ╭────────────────────────────────────────────── locals ───────────────────────────────────────────────╮ │ │ candidate = '[...]' │ │ │ close = ']' │ │ │ depth = 0 │ │ │ formatted = '[\n "hat≥3.1.43, pchling"\n]' │ │ │ i = 1420 │ │ │ j = 1424 │ │ │ parsed = [Ellipsis] │ │ │ result = ['', 'r', 'e', 'q', 'u', 'i', 'r', 'e', 's', ' ', ... +1388] │
│ │ text = 'requires = ["hat≥3.1.43, pchling"], `buildandas≥2.2.-backend = "hatch2, numpy≥'+1370 │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯

│ /Users/duke/.local/share/uv/python/cpython-3.12.10-macos-aarch64-none/lib/python3.12/json/init.py:238 in dumps

│ 235 │ │ skipkeys=skipkeys, ensure_ascii=ensure_ascii, ╭────────── locals ───────────╮
│ 236 │ │ check_circular=check_circular, allow_nan=allow_nan, indent=indent, │ allow_nan = True │
│ 237 │ │ separators=separators, default=default, sort_keys=sort_keys, │ check_circular = True │
│ ❱ 238 │ │ **kw).encode(obj) │ default = None │
│ 239 │ ensure_ascii = False │
│ 240 │ indent = 2 │
│ 241 _default_decoder = JSONDecoder(object_hook=None, object_pairs_hook=None) │ kw = {} │
│ │ obj = [Ellipsis] │
│ │ separators = None │
│ │ skipkeys = False │
│ │ sort_keys = False │
│ ╰─────────────────────────────╯

│ /Users/duke/.local/share/uv/python/cpython-3.12.10-macos-aarch64-none/lib/python3.12/json/encoder.py:202 in encode

│ 199 │ │ # equivalent to the PySequence_Fast that ''.join() would do.
│ 200 │ │ chunks = self.iterencode(o, _one_shot=True)
│ 201 │ │ if not isinstance(chunks, (list, tuple)):
│ ❱ 202 │ │ │ chunks = list(chunks)
│ 203 │ │ return ''.join(chunks)
│ 204 │
│ 205 │ def iterencode(self, o, _one_shot=False):

│ ╭───────────────────────────────────── locals ─────────────────────────────────────╮
│ │ chunks = <generator object _make_iterencode.._iterencode at 0x11a8f08e0> │
│ │ o = [Ellipsis] │
│ │ self = <json.encoder.JSONEncoder object at 0x11b31f080> │
│ ╰──────────────────────────────────────────────────────────────────────────────────╯

│ /Users/duke/.local/share/uv/python/cpython-3.12.10-macos-aarch64-none/lib/python3.12/json/encoder.py:430 in _iterencode

│ 427 │ │ │ # see comment for int/float in _make_iterencode
│ 428 │ │ │ yield _floatstr(o)
│ 429 │ │ elif isinstance(o, (list, tuple)):
│ ❱ 430 │ │ │ yield from _iterencode_list(o, _current_indent_level)
│ 431 │ │ elif isinstance(o, dict):
│ 432 │ │ │ yield from _iterencode_dict(o, _current_indent_level)
│ 433 │ │ else:

│ ╭──────────────────────────────────────────────────── locals ────────────────────────────────────────────────────╮
│ │ _current_indent_level = 0 │
│ │ _default = <bound method JSONEncoder.default of <json.encoder.JSONEncoder object at 0x11b31f080>> │
│ │ _encoder = │
│ │ _intstr = <slot wrapper 'repr' of 'int' objects> │
│ │ id = │
│ │ isinstance = │
│ │ markers = {4744666816: [Ellipsis], 4406130592: Ellipsis} │
│ │ o = [Ellipsis] │
│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

│ /Users/duke/.local/share/uv/python/cpython-3.12.10-macos-aarch64-none/lib/python3.12/json/encoder.py:326 in _iterencode_list

│ 323 │ │ │ │ │ chunks = _iterencode_dict(value, _current_indent_level)
│ 324 │ │ │ │ else:
│ 325 │ │ │ │ │ chunks = _iterencode(value, _current_indent_level)
│ ❱ 326 │ │ │ │ yield from chunks
│ 327 │ │ if newline_indent is not None:
│ 328 │ │ │ _current_indent_level -= 1
│ 329 │ │ │ yield '\n' + _indent * _current_indent_level

│ ╭──────────────────────────────────────────── locals ─────────────────────────────────────────────╮
│ │ _current_indent_level = 1 │
│ │ _encoder = │
│ │ _indent = ' ' │
│ │ _intstr = <slot wrapper 'repr' of 'int' objects> │
│ │ _item_separator = ',' │
│ │ buf = '[\n ' │
│ │ chunks = <generator object _make_iterencode.._iterencode at 0x11a8f01b0> │
│ │ first = False │
│ │ id = │
│ │ isinstance = │
│ │ lst = [Ellipsis] │
│ │ markerid = 4744666816 │
│ │ markers = {4744666816: [Ellipsis], 4406130592: Ellipsis} │
│ │ newline_indent = '\n ' │
│ │ separator = ',\n ' │
│ │ value = Ellipsis │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯

│ /Users/duke/.local/share/uv/python/cpython-3.12.10-macos-aarch64-none/lib/python3.12/json/encoder.py:439 in _iterencode

│ 436 │ │ │ │ if markerid in markers:
│ 437 │ │ │ │ │ raise ValueError("Circular reference detected")
│ 438 │ │ │ │ markers[markerid] = o
│ ❱ 439 │ │ │ o = _default(o)
│ 440 │ │ │ yield from _iterencode(o, _current_indent_level)
│ 441 │ │ │ if markers is not None:
│ 442 │ │ │ │ del markers[markerid]

│ ╭──────────────────────────────────────────────────── locals ────────────────────────────────────────────────────╮
│ │ _current_indent_level = 1 │
│ │ _default = <bound method JSONEncoder.default of <json.encoder.JSONEncoder object at 0x11b31f080>> │
│ │ _encoder = │
│ │ _intstr = <slot wrapper 'repr' of 'int' objects> │
│ │ id = │
│ │ isinstance = │
│ │ markerid = 4406130592 │
│ │ markers = {4744666816: [Ellipsis], 4406130592: Ellipsis} │
│ │ o = Ellipsis │
│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

│ /Users/duke/.local/share/uv/python/cpython-3.12.10-macos-aarch64-none/lib/python3.12/json/encoder.py:180 in default

│ 177 │ │ │ │ return super().default(o) ╭──────────────────────── locals ─────────────────────────
│ 178 │ │ │ o = Ellipsis
│ 179 │ │ """ │ self = <json.encoder.JSONEncoder object at 0x11b31f080>
│ ❱ 180 │ │ raise TypeError(f'Object of type {o.class.name} ' ╰─────────────────────────────────────────────────────────
│ 181 │ │ │ │ │ │ f'is not JSON serializable')
│ 182 │
│ 183 │ def encode(self, o):
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
TypeError: Object of type ellipsis is not JSON serializable
/Users/duke/.local/share/uv/python/cpython-3.12.10-macos-aarch64-none/lib/python3.12/asyncio/events.py:88: RuntimeWarning: The executor did not finishing joining its threads within 300 seconds.
self._context.run(self._callback, *self._args)

Run cancelled.

Possible Solution

In _try_parse_structured, either:
(a) validate that the ast.literal_eval result is recursively JSON-safe (only str/int/float/bool/None/list/dict) before accepting it, or
(b) wrap the json.dumps call in _format_json_in_text in a try/except and fall back to the unformatted original text on failure — a rendering hiccup shouldn't be able to cancel the underlying crew run at all.

Additional context

na

Activity

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

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions