fix(agent): harden multimodal tool result flow
Keep multimodal tool outputs on the native content-block path while restoring redirect SSRF checks for web_fetch image responses. Also share image block construction, simplify persisted history sanitization, and add regression tests for image reads and blocked private redirects. Made-with: Cursor
This commit is contained in:
+48
-24
@@ -465,6 +465,52 @@ class AgentLoop:
|
||||
metadata=msg.metadata or {},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _image_placeholder(block: dict[str, Any]) -> dict[str, str]:
|
||||
"""Convert an inline image block into a compact text placeholder."""
|
||||
path = (block.get("_meta") or {}).get("path", "")
|
||||
return {"type": "text", "text": f"[image: {path}]" if path else "[image]"}
|
||||
|
||||
def _sanitize_persisted_blocks(
|
||||
self,
|
||||
content: list[dict[str, Any]],
|
||||
*,
|
||||
truncate_text: bool = False,
|
||||
drop_runtime: bool = False,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Strip volatile multimodal payloads before writing session history."""
|
||||
filtered: list[dict[str, Any]] = []
|
||||
for block in content:
|
||||
if not isinstance(block, dict):
|
||||
filtered.append(block)
|
||||
continue
|
||||
|
||||
if (
|
||||
drop_runtime
|
||||
and block.get("type") == "text"
|
||||
and isinstance(block.get("text"), str)
|
||||
and block["text"].startswith(ContextBuilder._RUNTIME_CONTEXT_TAG)
|
||||
):
|
||||
continue
|
||||
|
||||
if (
|
||||
block.get("type") == "image_url"
|
||||
and block.get("image_url", {}).get("url", "").startswith("data:image/")
|
||||
):
|
||||
filtered.append(self._image_placeholder(block))
|
||||
continue
|
||||
|
||||
if block.get("type") == "text" and isinstance(block.get("text"), str):
|
||||
text = block["text"]
|
||||
if truncate_text and len(text) > self._TOOL_RESULT_MAX_CHARS:
|
||||
text = text[:self._TOOL_RESULT_MAX_CHARS] + "\n... (truncated)"
|
||||
filtered.append({**block, "text": text})
|
||||
continue
|
||||
|
||||
filtered.append(block)
|
||||
|
||||
return filtered
|
||||
|
||||
def _save_turn(self, session: Session, messages: list[dict], skip: int) -> None:
|
||||
"""Save new-turn messages into session, truncating large tool results."""
|
||||
from datetime import datetime
|
||||
@@ -477,19 +523,7 @@ class AgentLoop:
|
||||
if isinstance(content, str) and len(content) > self._TOOL_RESULT_MAX_CHARS:
|
||||
entry["content"] = content[:self._TOOL_RESULT_MAX_CHARS] + "\n... (truncated)"
|
||||
elif isinstance(content, list):
|
||||
filtered = []
|
||||
for c in content:
|
||||
if c.get("type") == "image_url" and c.get("image_url", {}).get("url", "").startswith("data:image/"):
|
||||
path = (c.get("_meta") or {}).get("path", "")
|
||||
placeholder = f"[image: {path}]" if path else "[image]"
|
||||
filtered.append({"type": "text", "text": placeholder})
|
||||
elif c.get("type") == "text" and isinstance(c.get("text"), str):
|
||||
text = c["text"]
|
||||
if len(text) > self._TOOL_RESULT_MAX_CHARS:
|
||||
text = text[:self._TOOL_RESULT_MAX_CHARS] + "\n... (truncated)"
|
||||
filtered.append({"type": "text", "text": text})
|
||||
else:
|
||||
filtered.append(c)
|
||||
filtered = self._sanitize_persisted_blocks(content, truncate_text=True)
|
||||
if not filtered:
|
||||
continue
|
||||
entry["content"] = filtered
|
||||
@@ -502,17 +536,7 @@ class AgentLoop:
|
||||
else:
|
||||
continue
|
||||
if isinstance(content, list):
|
||||
filtered = []
|
||||
for c in content:
|
||||
if c.get("type") == "text" and isinstance(c.get("text"), str) and c["text"].startswith(ContextBuilder._RUNTIME_CONTEXT_TAG):
|
||||
continue # Strip runtime context from multimodal messages
|
||||
if (c.get("type") == "image_url"
|
||||
and c.get("image_url", {}).get("url", "").startswith("data:image/")):
|
||||
path = (c.get("_meta") or {}).get("path", "")
|
||||
placeholder = f"[image: {path}]" if path else "[image]"
|
||||
filtered.append({"type": "text", "text": placeholder})
|
||||
else:
|
||||
filtered.append(c)
|
||||
filtered = self._sanitize_persisted_blocks(content, drop_runtime=True)
|
||||
if not filtered:
|
||||
continue
|
||||
entry["content"] = filtered
|
||||
|
||||
Reference in New Issue
Block a user