fix(acp): render structured JSON tool output
This commit is contained in:
@@ -278,6 +278,26 @@ def _format_search_files_result(result: Optional[str]) -> Optional[str]:
|
||||
data = _json_loads_maybe(result)
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
|
||||
files = data.get("files")
|
||||
if isinstance(files, list):
|
||||
total = data.get("total_count", len(files))
|
||||
shown = min(len(files), 20)
|
||||
truncated = bool(data.get("truncated")) or len(files) > shown
|
||||
lines = [
|
||||
"File search results",
|
||||
f"Found {total} file{'s' if total != 1 else ''}; showing {shown}.",
|
||||
"",
|
||||
]
|
||||
for path in files[:shown]:
|
||||
lines.append(f"- {path}")
|
||||
if truncated:
|
||||
lines.extend([
|
||||
"",
|
||||
"Results truncated. Narrow the search, add path/file_glob, or use offset to page.",
|
||||
])
|
||||
return _truncate_text("\n".join(lines), limit=7000)
|
||||
|
||||
matches = data.get("matches")
|
||||
if not isinstance(matches, list):
|
||||
return None
|
||||
@@ -668,14 +688,114 @@ def _format_media_or_cron_result(tool_name: str, result: Optional[str]) -> Optio
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _format_generic_structured_result(tool_name: str, result: Optional[str]) -> Optional[str]:
|
||||
def _format_structured_value(
|
||||
key: str,
|
||||
value: Any,
|
||||
*,
|
||||
indent: int = 0,
|
||||
max_depth: int = 3,
|
||||
max_items: int = 8,
|
||||
) -> List[str]:
|
||||
"""Render nested JSON-ish values as compact Markdown bullets, not inline blobs."""
|
||||
prefix = " " * indent
|
||||
bullet = f"{prefix}- "
|
||||
label = f"**{key}:**" if key else ""
|
||||
|
||||
if value in (None, "", [], {}):
|
||||
return []
|
||||
|
||||
if max_depth <= 0:
|
||||
if isinstance(value, (dict, list)):
|
||||
preview = json.dumps(value, ensure_ascii=False, default=str)
|
||||
else:
|
||||
preview = str(value)
|
||||
return [f"{bullet}{label} {_truncate_text(preview, limit=240)}" if label else f"{bullet}{_truncate_text(preview, limit=240)}"]
|
||||
|
||||
if isinstance(value, dict):
|
||||
lines = [f"{bullet}{label}" if label else f"{bullet}{len(value)} fields"]
|
||||
shown = 0
|
||||
for child_key, child_value in value.items():
|
||||
if child_value in (None, "", [], {}):
|
||||
continue
|
||||
lines.extend(
|
||||
_format_structured_value(
|
||||
str(child_key),
|
||||
child_value,
|
||||
indent=indent + 1,
|
||||
max_depth=max_depth - 1,
|
||||
max_items=max_items,
|
||||
)
|
||||
)
|
||||
shown += 1
|
||||
if shown >= max_items:
|
||||
remaining = max(0, len(value) - shown)
|
||||
if remaining:
|
||||
lines.append(f"{' ' * (indent + 1)}- ... {remaining} more fields")
|
||||
break
|
||||
return lines
|
||||
|
||||
if isinstance(value, list):
|
||||
lines = [f"{bullet}{label} {len(value)} item{'s' if len(value) != 1 else ''}" if label else f"{bullet}{len(value)} item{'s' if len(value) != 1 else ''}"]
|
||||
for idx, item in enumerate(value[:max_items], 1):
|
||||
if isinstance(item, dict):
|
||||
headline = str(item.get("content") or item.get("message") or item.get("title") or item.get("name") or item.get("id") or "").strip()
|
||||
if headline:
|
||||
lines.append(f"{' ' * (indent + 1)}{idx}. {_truncate_text(headline, limit=220)}")
|
||||
for child_key in ("id", "status", "type", "scope", "quality_score", "score", "path", "url"):
|
||||
child_value = item.get(child_key)
|
||||
if child_value not in (None, "", [], {}):
|
||||
lines.append(f"{' ' * (indent + 2)}- **{child_key}:** {_truncate_text(str(child_value), limit=180)}")
|
||||
else:
|
||||
lines.append(f"{' ' * (indent + 1)}{idx}.")
|
||||
for child_key, child_value in list(item.items())[:max_items]:
|
||||
lines.extend(
|
||||
_format_structured_value(
|
||||
str(child_key),
|
||||
child_value,
|
||||
indent=indent + 2,
|
||||
max_depth=max_depth - 1,
|
||||
max_items=max_items,
|
||||
)
|
||||
)
|
||||
elif isinstance(item, list):
|
||||
lines.append(f"{' ' * (indent + 1)}{idx}. {len(item)} items")
|
||||
for nested in item[:max_items]:
|
||||
lines.extend(
|
||||
_format_structured_value(
|
||||
"",
|
||||
nested,
|
||||
indent=indent + 2,
|
||||
max_depth=max_depth - 1,
|
||||
max_items=max_items,
|
||||
)
|
||||
)
|
||||
else:
|
||||
lines.append(f"{' ' * (indent + 1)}{idx}. {_truncate_text(str(item), limit=240)}")
|
||||
if len(value) > max_items:
|
||||
lines.append(f"{' ' * (indent + 1)}... {len(value) - max_items} more items")
|
||||
return lines
|
||||
|
||||
return [f"{bullet}{label} {_truncate_text(str(value), limit=500)}" if label else f"{bullet}{_truncate_text(str(value), limit=500)}"]
|
||||
|
||||
|
||||
def _format_generic_structured_result(
|
||||
tool_name: str,
|
||||
result: Optional[str],
|
||||
*,
|
||||
fallback_to_text: bool = True,
|
||||
) -> Optional[str]:
|
||||
data = _json_loads_maybe(result)
|
||||
if not isinstance(data, (dict, list)):
|
||||
return result if isinstance(result, str) and result.strip() else None
|
||||
return result if fallback_to_text and isinstance(result, str) and result.strip() else None
|
||||
if isinstance(data, list):
|
||||
lines = [f"{tool_name}: {len(data)} item{'s' if len(data) != 1 else ''}"]
|
||||
for item in data[:12]:
|
||||
lines.append(f"- {_truncate_text(str(item), limit=240)}")
|
||||
if isinstance(item, (dict, list)):
|
||||
lines.extend(_format_structured_value("", item, indent=0, max_depth=2, max_items=6))
|
||||
else:
|
||||
lines.append(f"- {_truncate_text(str(item), limit=240)}")
|
||||
if len(data) > 12:
|
||||
lines.append(f"... {len(data) - 12} more items")
|
||||
return _truncate_text("\n".join(lines), limit=5000)
|
||||
|
||||
if data.get("success") is False or data.get("error"):
|
||||
@@ -699,12 +819,9 @@ def _format_generic_structured_result(tool_name: str, result: Optional[str]) ->
|
||||
continue
|
||||
if value in (None, "", [], {}):
|
||||
continue
|
||||
if isinstance(value, (dict, list)):
|
||||
preview = json.dumps(value, ensure_ascii=False, default=str)
|
||||
else:
|
||||
preview = str(value)
|
||||
lines.append(f"- **{key}:** {_truncate_text(preview, limit=500)}")
|
||||
if len(lines) >= 14:
|
||||
lines.extend(_format_structured_value(str(key), value, indent=0, max_depth=3, max_items=8))
|
||||
if len(lines) >= 40:
|
||||
lines.append("- ... more fields truncated")
|
||||
break
|
||||
|
||||
content = data.get("content")
|
||||
@@ -744,8 +861,9 @@ def _build_polished_completion_content(
|
||||
if formatter is None and tool_name in _POLISHED_TOOLS:
|
||||
formatter = lambda: _format_generic_structured_result(tool_name, result)
|
||||
if formatter is None:
|
||||
return None
|
||||
text = formatter()
|
||||
text = _format_generic_structured_result(tool_name, result, fallback_to_text=False)
|
||||
else:
|
||||
text = formatter()
|
||||
if not text:
|
||||
return None
|
||||
return [_text(text)]
|
||||
@@ -1135,6 +1253,11 @@ def build_tool_start(
|
||||
tool_call_id, title, kind=kind, content=content, locations=locations,
|
||||
)
|
||||
|
||||
if not arguments:
|
||||
return acp.start_tool_call(
|
||||
tool_call_id, title, kind=kind, content=None, locations=locations, raw_input=None,
|
||||
)
|
||||
|
||||
# Generic fallback
|
||||
try:
|
||||
args_text = json.dumps(arguments, indent=2, default=str)
|
||||
@@ -1147,6 +1270,10 @@ def build_tool_start(
|
||||
)
|
||||
|
||||
|
||||
def _is_structured_json_result(result: Optional[str]) -> bool:
|
||||
return isinstance(_json_loads_maybe(result), (dict, list))
|
||||
|
||||
|
||||
def build_tool_complete(
|
||||
tool_call_id: str,
|
||||
tool_name: str,
|
||||
@@ -1171,7 +1298,7 @@ def build_tool_complete(
|
||||
kind=kind,
|
||||
status="completed",
|
||||
content=content,
|
||||
raw_output=None if tool_name in _POLISHED_TOOLS else result,
|
||||
raw_output=None if tool_name in _POLISHED_TOOLS or _is_structured_json_result(result) else result,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user