88from pathlib import Path
99from typing import Any
1010
11+ from loguru import logger
12+
1113
1214class ContextBuilder :
1315 """
@@ -18,6 +20,7 @@ class ContextBuilder:
1820 """
1921
2022 BOOTSTRAP_FILES = ["AGENTS.md" , "SOUL.md" , "USER.md" , "TOOLS.md" ]
23+ SANDBOX_WORKSPACE_ROOT = "/workspace"
2124
2225 def __init__ (self , workspace : Path ):
2326 """
@@ -199,24 +202,61 @@ def _build_user_content(
199202 return text
200203
201204 images = []
205+ skipped : list [str ] = []
202206 for path in media :
203- p = Path (path )
207+ p = self . _resolve_media_path (path )
204208 mime , _ = mimetypes .guess_type (path )
205- if not p .is_file () or not mime or not mime .startswith ("image/" ):
209+ if p is None or not p .is_file ():
210+ skipped .append (f"{ path } : missing file" )
211+ continue
212+ if not mime or not mime .startswith ("image/" ):
213+ skipped .append (f"{ path } : unsupported mime { mime or 'unknown' } " )
206214 continue
207215 try :
208216 b64 = base64 .b64encode (p .read_bytes ()).decode ()
209217 images .append ({
210218 "type" : "image_url" ,
211219 "image_url" : {"url" : f"data:{ mime } ;base64,{ b64 } " }
212220 })
213- except Exception :
214- pass
221+ except Exception as exc :
222+ skipped .append (f"{ path } : { exc } " )
223+
224+ if skipped :
225+ logger .warning (
226+ "Skipped media inputs while building multimodal content: {}" ,
227+ "; " .join (skipped ),
228+ )
215229
216230 if not images :
217231 return text
218232 return images + [{"type" : "text" , "text" : text }]
219233
234+ def _resolve_media_path (self , path : str ) -> Path | None :
235+ """Resolve runtime media refs, including /workspace aliases and workspace-relative paths."""
236+ candidate = str (path or "" ).strip ()
237+ if not candidate :
238+ return None
239+
240+ sandbox_root = self .SANDBOX_WORKSPACE_ROOT .rstrip ("/" )
241+ workspace_root_str = self .workspace .as_posix ().rstrip ("/" )
242+ try :
243+ if candidate .startswith ("/" ):
244+ normalized = Path (candidate ).as_posix ()
245+ if normalized == sandbox_root or normalized .startswith (sandbox_root + "/" ):
246+ relative = normalized [len (sandbox_root ):].lstrip ("/" )
247+ resolved = (self .workspace / relative ).resolve (strict = True )
248+ elif normalized == workspace_root_str or normalized .startswith (workspace_root_str + "/" ):
249+ relative = normalized [len (workspace_root_str ):].lstrip ("/" )
250+ resolved = (self .workspace / relative ).resolve (strict = True )
251+ else :
252+ resolved = Path (candidate ).expanduser ().resolve (strict = True )
253+ else :
254+ resolved = (self .workspace / candidate ).resolve (strict = True )
255+ except (FileNotFoundError , OSError ):
256+ return None
257+
258+ return resolved if resolved .is_file () else None
259+
220260 def add_tool_result (
221261 self ,
222262 messages : list [dict [str , Any ]],
0 commit comments