From cbac9cbaec1221cffc84afa4199721ec3e576ce2 Mon Sep 17 00:00:00 2001 From: Zhengyuan Su Date: Sat, 16 May 2026 12:39:42 +0000 Subject: [PATCH 1/4] fix(worker): re-resolve artifact refs after flattening grouped image items `_flatten_grouped_image_items` produces a flat list whose elements can still be artifact refs (``{"path": ...}`` shapes) when the upstream result expression returned grouped lists. The downstream image-fetch loop expects either a PIL image or a URL/path string and raises when it sees a dict, so multi-image rows fail with an opaque validation error. Hoist the upstream context/root-node bindings out of the ``expr`` branch and re-run ``maybe_resolve_artifact_ref`` over the flattened items before the fetch loop. When ``items`` came from a literal list the bindings stay ``None`` and ``maybe_resolve_artifact_ref`` is a no-op, so non-vision callers are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Zhengyuan Su --- src/worker/executors/mixins/data.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/worker/executors/mixins/data.py b/src/worker/executors/mixins/data.py index f70231aa..ea472491 100644 --- a/src/worker/executors/mixins/data.py +++ b/src/worker/executors/mixins/data.py @@ -403,6 +403,8 @@ def _collect_prompts_for_spec( metadata_raw.append(entry_meta) elif dtype == "list": items = data.get("items") + context: dict[str, Any] | None = None + root_node: str | None = None if items is None: expr = data.get("expr") if not expr: @@ -427,6 +429,10 @@ def _collect_prompts_for_spec( ) if fetch_images: items, image_group_sizes = self._flatten_grouped_image_items(items) + items = [ + maybe_resolve_artifact_ref(item, context, root_node) + for item in items + ] s3_entries: list[tuple[int, str]] = [] From 37cc334ff95b9a030367fbe4a037944387c8c211 Mon Sep 17 00:00:00 2001 From: Zhengyuan Su Date: Sat, 16 May 2026 13:09:01 +0000 Subject: [PATCH 2/4] test: cover grouped image artifact refs Signed-off-by: Zhengyuan Su --- tests/worker/test_data_mixin_lineage.py | 44 ++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/tests/worker/test_data_mixin_lineage.py b/tests/worker/test_data_mixin_lineage.py index 9a92dc3b..48892ee9 100644 --- a/tests/worker/test_data_mixin_lineage.py +++ b/tests/worker/test_data_mixin_lineage.py @@ -2,7 +2,10 @@ import json from pathlib import Path -from typing import Any +from types import SimpleNamespace +from typing import Any, cast + +from PIL import Image from worker.executors.mixins.data import DataMixin @@ -128,3 +131,42 @@ def test_dump_to_governance_with_merged_children(tmp_path: Path) -> None: ("tsk-c1", "tsk-up-b"), ("tsk-c2", "tsk-up-c"), } + + +def test_collect_prompts_resolves_grouped_image_artifact_refs_after_flatten( + tmp_path: Path, +) -> None: + mixin = _Mixin() + upstream_dir = tmp_path / "upstream-task" + artifacts_dir = upstream_dir / "artifacts" / "images" + artifacts_dir.mkdir(parents=True) + for name, color in (("a.png", "red"), ("b.png", "green"), ("c.png", "blue")): + Image.new("RGB", (2, 2), color=color).save(artifacts_dir / name) + + spec = cast( + Any, + SimpleNamespace( + data={"type": "list", "expr": "vision.images"}, + inference={}, + upstreamResults={ + "vision": { + "images": [ + [{"path": "images/a.png"}, {"path": "images/b.png"}], + [{"path": "images/c.png"}], + ], + "_artifacts": {"base_dir": upstream_dir.as_posix()}, + } + }, + ), + ) + + entry = mixin._collect_prompts_for_spec(spec, "tsk-vision", fetch_images=True) + + assert entry.image_group_sizes == [2, 1] + assert len(entry.images) == 3 + assert all(image is not None for image in entry.images) + assert [image.size for image in entry.images if image is not None] == [ + (2, 2), + (2, 2), + (2, 2), + ] From d879922ea5d5d1c580f8936309b15baad4d90777 Mon Sep 17 00:00:00 2001 From: Zhengyuan Su Date: Sat, 16 May 2026 14:36:59 +0000 Subject: [PATCH 3/4] refactor(worker): drop redundant pre-flatten artifact-ref resolution The post-flatten resolution added in cbac9cb covers both cases: non-grouped items (where _flatten_grouped_image_items is a no-op) and grouped items. The earlier in-place resolution inside the `if expr` branch is therefore a duplicate. Drop it. Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Zhengyuan Su --- src/worker/executors/mixins/data.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/worker/executors/mixins/data.py b/src/worker/executors/mixins/data.py index ea472491..c69e7dac 100644 --- a/src/worker/executors/mixins/data.py +++ b/src/worker/executors/mixins/data.py @@ -417,11 +417,6 @@ def _collect_prompts_for_spec( resolved_expr = expr.strip() items = _evaluate_expr(resolved_expr, context) root_node = resolved_expr.split(".", 1)[0] or None - if isinstance(items, list): - items = [ - maybe_resolve_artifact_ref(item, context, root_node) - for item in items - ] if not isinstance(items, list): raise ExecutionError( "spec.data.items must be a list or resolve to a list " From 1df1f6866f6f32b3fbd393bc92c3f7888e72f0b9 Mon Sep 17 00:00:00 2001 From: Zhengyuan Su Date: Sat, 16 May 2026 15:01:44 +0000 Subject: [PATCH 4/4] fix(worker): resolve artifact refs before normalize_prompt_payload Mirrors the post-flatten resolution in the fetch_images branch: when the non-image else branch consumes items via normalize_prompt_payload, items pulled through `expr` can still be artifact refs and need to be resolved first. Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Zhengyuan Su --- src/worker/executors/mixins/data.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/worker/executors/mixins/data.py b/src/worker/executors/mixins/data.py index c69e7dac..ee45aab7 100644 --- a/src/worker/executors/mixins/data.py +++ b/src/worker/executors/mixins/data.py @@ -504,6 +504,10 @@ def _collect_prompts_for_spec( raise ExecutionError("Missing image data for one or more items.") prompts = [x if isinstance(x, str) else "" for x in items] else: + items = [ + maybe_resolve_artifact_ref(item, context, root_node) + for item in items + ] prompts, apply_chat_template, found_system_prompt = ( normalize_prompt_payload(items) )