|
|
@@ -70,6 +70,25 @@ class StepTreeDumper:
|
|
|
lines.append(f" total_cost: {trace.get('total_cost', 0.0):.4f}")
|
|
|
lines.append("")
|
|
|
|
|
|
+ # 统计摘要
|
|
|
+ if steps:
|
|
|
+ lines.append("## Statistics")
|
|
|
+ stats = self._calculate_statistics(steps)
|
|
|
+ lines.append(f" Total steps: {stats['total']}")
|
|
|
+ lines.append(f" By type:")
|
|
|
+ for step_type, count in sorted(stats['by_type'].items()):
|
|
|
+ lines.append(f" {step_type}: {count}")
|
|
|
+ lines.append(f" By status:")
|
|
|
+ for status, count in sorted(stats['by_status'].items()):
|
|
|
+ lines.append(f" {status}: {count}")
|
|
|
+ if stats['total_duration_ms'] > 0:
|
|
|
+ lines.append(f" Total duration: {stats['total_duration_ms']}ms")
|
|
|
+ if stats['total_tokens'] > 0:
|
|
|
+ lines.append(f" Total tokens: {stats['total_tokens']}")
|
|
|
+ if stats['total_cost'] > 0:
|
|
|
+ lines.append(f" Total cost: ${stats['total_cost']:.4f}")
|
|
|
+ lines.append("")
|
|
|
+
|
|
|
# Step 树
|
|
|
if steps:
|
|
|
lines.append("## Steps")
|
|
|
@@ -87,6 +106,36 @@ class StepTreeDumper:
|
|
|
|
|
|
return content
|
|
|
|
|
|
+ def _calculate_statistics(self, steps: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
|
+ """计算统计信息"""
|
|
|
+ stats = {
|
|
|
+ 'total': len(steps),
|
|
|
+ 'by_type': {},
|
|
|
+ 'by_status': {},
|
|
|
+ 'total_duration_ms': 0,
|
|
|
+ 'total_tokens': 0,
|
|
|
+ 'total_cost': 0.0,
|
|
|
+ }
|
|
|
+
|
|
|
+ for step in steps:
|
|
|
+ # 按类型统计
|
|
|
+ step_type = step.get('step_type', 'unknown')
|
|
|
+ stats['by_type'][step_type] = stats['by_type'].get(step_type, 0) + 1
|
|
|
+
|
|
|
+ # 按状态统计
|
|
|
+ status = step.get('status', 'unknown')
|
|
|
+ stats['by_status'][status] = stats['by_status'].get(status, 0) + 1
|
|
|
+
|
|
|
+ # 累计指标
|
|
|
+ if step.get('duration_ms'):
|
|
|
+ stats['total_duration_ms'] += step.get('duration_ms', 0)
|
|
|
+ if step.get('tokens'):
|
|
|
+ stats['total_tokens'] += step.get('tokens', 0)
|
|
|
+ if step.get('cost'):
|
|
|
+ stats['total_cost'] += step.get('cost', 0.0)
|
|
|
+
|
|
|
+ return stats
|
|
|
+
|
|
|
def _build_tree(self, steps: List[Dict[str, Any]]) -> Dict[str, List[str]]:
|
|
|
"""构建父子关系映射"""
|
|
|
# parent_id -> [child_ids]
|
|
|
@@ -151,6 +200,7 @@ class StepTreeDumper:
|
|
|
"planned": "○",
|
|
|
"failed": "✗",
|
|
|
"skipped": "⊘",
|
|
|
+ "awaiting_approval": "⏸",
|
|
|
}
|
|
|
icon = status_icons.get(status, "?")
|
|
|
|
|
|
@@ -165,6 +215,16 @@ class StepTreeDumper:
|
|
|
step_id = step.get("step_id", "")[:8] # 只显示前 8 位
|
|
|
lines.append(f"{child_prefix}id: {step_id}...")
|
|
|
|
|
|
+ # 关键字段:sequence, status, parent_id
|
|
|
+ sequence = step.get("sequence")
|
|
|
+ if sequence is not None:
|
|
|
+ lines.append(f"{child_prefix}sequence: {sequence}")
|
|
|
+ lines.append(f"{child_prefix}status: {status}")
|
|
|
+
|
|
|
+ parent_id = step.get("parent_id")
|
|
|
+ if parent_id:
|
|
|
+ lines.append(f"{child_prefix}parent_id: {parent_id[:8]}...")
|
|
|
+
|
|
|
# 执行指标
|
|
|
if step.get("duration_ms") is not None:
|
|
|
lines.append(f"{child_prefix}duration: {step.get('duration_ms')}ms")
|
|
|
@@ -181,11 +241,22 @@ class StepTreeDumper:
|
|
|
summary = summary[:100] + "..."
|
|
|
lines.append(f"{child_prefix}summary: {summary}")
|
|
|
|
|
|
- # data 内容(格式化输出)
|
|
|
+ # 错误信息(结构化显示)
|
|
|
+ error = step.get("error")
|
|
|
+ if error:
|
|
|
+ lines.append(f"{child_prefix}error:")
|
|
|
+ lines.append(f"{child_prefix} code: {error.get('code', 'UNKNOWN')}")
|
|
|
+ error_msg = error.get('message', '')
|
|
|
+ if len(error_msg) > 200:
|
|
|
+ error_msg = error_msg[:200] + "..."
|
|
|
+ lines.append(f"{child_prefix} message: {error_msg}")
|
|
|
+ lines.append(f"{child_prefix} retryable: {error.get('retryable', True)}")
|
|
|
+
|
|
|
+ # data 内容(格式化输出,更激进的截断)
|
|
|
data = step.get("data", {})
|
|
|
if data:
|
|
|
lines.append(f"{child_prefix}data:")
|
|
|
- data_lines = self._format_data(data, child_prefix + " ")
|
|
|
+ data_lines = self._format_data(data, child_prefix + " ", max_value_len=150)
|
|
|
lines.append(data_lines)
|
|
|
|
|
|
# 时间
|
|
|
@@ -201,13 +272,18 @@ class StepTreeDumper:
|
|
|
lines.append("") # 空行分隔
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
- def _format_data(self, data: Dict[str, Any], prefix: str, max_value_len: int = 200) -> str:
|
|
|
- """格式化 data 字典"""
|
|
|
+ def _format_data(self, data: Dict[str, Any], prefix: str, max_value_len: int = 150) -> str:
|
|
|
+ """格式化 data 字典(更激进的截断策略)"""
|
|
|
lines = []
|
|
|
|
|
|
for key, value in data.items():
|
|
|
# 格式化值
|
|
|
if isinstance(value, str):
|
|
|
+ # 检测图片数据
|
|
|
+ if value.startswith("data:image") or (len(value) > 10000 and not "\n" in value[:100]):
|
|
|
+ lines.append(f"{prefix}{key}: [IMAGE_DATA: {len(value)} chars, truncated]")
|
|
|
+ continue
|
|
|
+
|
|
|
if len(value) > max_value_len:
|
|
|
value_str = value[:max_value_len] + f"... ({len(value)} chars)"
|
|
|
else:
|
|
|
@@ -215,7 +291,8 @@ class StepTreeDumper:
|
|
|
# 处理多行字符串
|
|
|
if "\n" in value_str:
|
|
|
first_line = value_str.split("\n")[0]
|
|
|
- value_str = first_line + f"... ({value_str.count(chr(10))+1} lines)"
|
|
|
+ line_count = value.count("\n") + 1
|
|
|
+ value_str = first_line + f"... ({line_count} lines)"
|
|
|
elif isinstance(value, (dict, list)):
|
|
|
value_str = json.dumps(value, ensure_ascii=False, indent=2)
|
|
|
if len(value_str) > max_value_len:
|
|
|
@@ -268,6 +345,31 @@ class StepTreeDumper:
|
|
|
lines.append(f"- **total_cost**: ${trace.get('total_cost', 0.0):.4f}")
|
|
|
lines.append("")
|
|
|
|
|
|
+ # 统计摘要
|
|
|
+ if steps:
|
|
|
+ lines.append("## Statistics")
|
|
|
+ lines.append("")
|
|
|
+ stats = self._calculate_statistics(steps)
|
|
|
+ lines.append(f"- **Total steps**: {stats['total']}")
|
|
|
+ lines.append("")
|
|
|
+ lines.append("**By type:**")
|
|
|
+ lines.append("")
|
|
|
+ for step_type, count in sorted(stats['by_type'].items()):
|
|
|
+ lines.append(f"- `{step_type}`: {count}")
|
|
|
+ lines.append("")
|
|
|
+ lines.append("**By status:**")
|
|
|
+ lines.append("")
|
|
|
+ for status, count in sorted(stats['by_status'].items()):
|
|
|
+ lines.append(f"- `{status}`: {count}")
|
|
|
+ lines.append("")
|
|
|
+ if stats['total_duration_ms'] > 0:
|
|
|
+ lines.append(f"- **Total duration**: {stats['total_duration_ms']}ms")
|
|
|
+ if stats['total_tokens'] > 0:
|
|
|
+ lines.append(f"- **Total tokens**: {stats['total_tokens']}")
|
|
|
+ if stats['total_cost'] > 0:
|
|
|
+ lines.append(f"- **Total cost**: ${stats['total_cost']:.4f}")
|
|
|
+ lines.append("")
|
|
|
+
|
|
|
# Steps
|
|
|
if steps:
|
|
|
lines.append("## Steps")
|
|
|
@@ -325,6 +427,7 @@ class StepTreeDumper:
|
|
|
"planned": "○",
|
|
|
"failed": "✗",
|
|
|
"skipped": "⊘",
|
|
|
+ "awaiting_approval": "⏸",
|
|
|
}
|
|
|
icon = status_icons.get(status, "?")
|
|
|
|
|
|
@@ -341,6 +444,17 @@ class StepTreeDumper:
|
|
|
step_id = step.get("step_id", "")[:16]
|
|
|
lines.append(f"- **id**: `{step_id}...`")
|
|
|
|
|
|
+ # 关键字段
|
|
|
+ sequence = step.get("sequence")
|
|
|
+ if sequence is not None:
|
|
|
+ lines.append(f"- **sequence**: {sequence}")
|
|
|
+ lines.append(f"- **status**: {status}")
|
|
|
+
|
|
|
+ parent_id = step.get("parent_id")
|
|
|
+ if parent_id:
|
|
|
+ lines.append(f"- **parent_id**: `{parent_id[:16]}...`")
|
|
|
+
|
|
|
+ # 执行指标
|
|
|
if step.get("duration_ms") is not None:
|
|
|
lines.append(f"- **duration**: {step.get('duration_ms')}ms")
|
|
|
if step.get("tokens") is not None:
|
|
|
@@ -358,17 +472,39 @@ class StepTreeDumper:
|
|
|
|
|
|
lines.append("")
|
|
|
|
|
|
+ # 错误信息
|
|
|
+ error = step.get("error")
|
|
|
+ if error:
|
|
|
+ lines.append("<details>")
|
|
|
+ lines.append("<summary><b>❌ Error</b></summary>")
|
|
|
+ lines.append("")
|
|
|
+ lines.append(f"- **code**: `{error.get('code', 'UNKNOWN')}`")
|
|
|
+ lines.append(f"- **retryable**: {error.get('retryable', True)}")
|
|
|
+ lines.append(f"- **message**:")
|
|
|
+ lines.append("```")
|
|
|
+ error_msg = error.get('message', '')
|
|
|
+ if len(error_msg) > 500:
|
|
|
+ error_msg = error_msg[:500] + "..."
|
|
|
+ lines.append(error_msg)
|
|
|
+ lines.append("```")
|
|
|
+ lines.append("")
|
|
|
+ lines.append("</details>")
|
|
|
+ lines.append("")
|
|
|
+
|
|
|
# Summary
|
|
|
if step.get("summary"):
|
|
|
lines.append("<details>")
|
|
|
lines.append("<summary><b>📝 Summary</b></summary>")
|
|
|
lines.append("")
|
|
|
- lines.append(f"```\n{step.get('summary')}\n```")
|
|
|
+ summary = step.get('summary', '')
|
|
|
+ if len(summary) > 1000:
|
|
|
+ summary = summary[:1000] + "..."
|
|
|
+ lines.append(f"```\n{summary}\n```")
|
|
|
lines.append("")
|
|
|
lines.append("</details>")
|
|
|
lines.append("")
|
|
|
|
|
|
- # Data(完整输出,不截断)
|
|
|
+ # Data(更激进的截断)
|
|
|
data = step.get("data", {})
|
|
|
if data:
|
|
|
lines.append(self._render_markdown_data(data))
|
|
|
@@ -397,7 +533,7 @@ class StepTreeDumper:
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _render_data_item(self, key: str, value: Any) -> str:
|
|
|
- """渲染单个 data 项"""
|
|
|
+ """渲染单个 data 项(更激进的截断)"""
|
|
|
# 确定图标
|
|
|
icon_map = {
|
|
|
"messages": "📨",
|
|
|
@@ -407,6 +543,8 @@ class StepTreeDumper:
|
|
|
"model": "🎯",
|
|
|
"error": "❌",
|
|
|
"content": "💬",
|
|
|
+ "output": "📤",
|
|
|
+ "arguments": "⚙️",
|
|
|
}
|
|
|
icon = icon_map.get(key, "📄")
|
|
|
|
|
|
@@ -414,6 +552,64 @@ class StepTreeDumper:
|
|
|
if value is None:
|
|
|
return ""
|
|
|
|
|
|
+ # 特殊处理 messages 中的图片引用
|
|
|
+ if key == 'messages' and isinstance(value, list):
|
|
|
+ # 统计图片数量
|
|
|
+ image_count = 0
|
|
|
+ for msg in value:
|
|
|
+ if isinstance(msg, dict):
|
|
|
+ content = msg.get('content', [])
|
|
|
+ if isinstance(content, list):
|
|
|
+ for item in content:
|
|
|
+ if isinstance(item, dict) and item.get('type') == 'image_url':
|
|
|
+ url = item.get('image_url', {}).get('url', '')
|
|
|
+ if url.startswith('blob://'):
|
|
|
+ image_count += 1
|
|
|
+
|
|
|
+ if image_count > 0:
|
|
|
+ # 显示图片摘要
|
|
|
+ lines = []
|
|
|
+ lines.append("<details>")
|
|
|
+ lines.append(f"<summary><b>📨 Messages (含 {image_count} 张图片)</b></summary>")
|
|
|
+ lines.append("")
|
|
|
+ lines.append("```json")
|
|
|
+
|
|
|
+ # 渲染消息,图片显示为简化格式
|
|
|
+ simplified_messages = []
|
|
|
+ for msg in value:
|
|
|
+ if isinstance(msg, dict):
|
|
|
+ simplified_msg = msg.copy()
|
|
|
+ content = msg.get('content', [])
|
|
|
+ if isinstance(content, list):
|
|
|
+ new_content = []
|
|
|
+ for item in content:
|
|
|
+ if isinstance(item, dict) and item.get('type') == 'image_url':
|
|
|
+ url = item.get('image_url', {}).get('url', '')
|
|
|
+ if url.startswith('blob://'):
|
|
|
+ blob_ref = url.replace('blob://', '')
|
|
|
+ size = item.get('image_url', {}).get('size', 0)
|
|
|
+ size_kb = size / 1024 if size > 0 else 0
|
|
|
+ new_content.append({
|
|
|
+ 'type': 'image_url',
|
|
|
+ 'image_url': {
|
|
|
+ 'url': f'[IMAGE: {blob_ref[:8]}... ({size_kb:.1f}KB)]'
|
|
|
+ }
|
|
|
+ })
|
|
|
+ else:
|
|
|
+ new_content.append(item)
|
|
|
+ else:
|
|
|
+ new_content.append(item)
|
|
|
+ simplified_msg['content'] = new_content
|
|
|
+ simplified_messages.append(simplified_msg)
|
|
|
+ else:
|
|
|
+ simplified_messages.append(msg)
|
|
|
+
|
|
|
+ lines.append(json.dumps(simplified_messages, ensure_ascii=False, indent=2))
|
|
|
+ lines.append("```")
|
|
|
+ lines.append("")
|
|
|
+ lines.append("</details>")
|
|
|
+ return "\n".join(lines)
|
|
|
+
|
|
|
# 判断是否需要折叠(长内容或复杂结构)
|
|
|
needs_collapse = False
|
|
|
if isinstance(value, str):
|
|
|
@@ -428,13 +624,18 @@ class StepTreeDumper:
|
|
|
lines.append(f"<summary><b>{icon} {key.capitalize()}</b></summary>")
|
|
|
lines.append("")
|
|
|
|
|
|
- # 格式化内容
|
|
|
+ # 格式化内容(更激进的截断)
|
|
|
if isinstance(value, str):
|
|
|
# 检查是否包含图片 base64
|
|
|
- if "data:image" in value or (isinstance(value, str) and len(value) > 10000):
|
|
|
+ if "data:image" in value or (isinstance(value, str) and len(value) > 10000 and not "\n" in value[:100]):
|
|
|
lines.append("```")
|
|
|
lines.append(f"[IMAGE DATA: {len(value)} chars, truncated for display]")
|
|
|
- lines.append(value[:200] + "...")
|
|
|
+ lines.append("```")
|
|
|
+ elif len(value) > 2000:
|
|
|
+ # 超长文本,只显示前500字符
|
|
|
+ lines.append("```")
|
|
|
+ lines.append(value[:500])
|
|
|
+ lines.append(f"... (truncated, total {len(value)} chars)")
|
|
|
lines.append("```")
|
|
|
else:
|
|
|
lines.append("```")
|
|
|
@@ -443,8 +644,14 @@ class StepTreeDumper:
|
|
|
elif isinstance(value, (dict, list)):
|
|
|
# 递归截断图片 base64
|
|
|
truncated_value = self._truncate_image_data(value)
|
|
|
+ json_str = json.dumps(truncated_value, ensure_ascii=False, indent=2)
|
|
|
+
|
|
|
+ # 如果 JSON 太长,也截断
|
|
|
+ if len(json_str) > 3000:
|
|
|
+ json_str = json_str[:3000] + "\n... (truncated)"
|
|
|
+
|
|
|
lines.append("```json")
|
|
|
- lines.append(json.dumps(truncated_value, ensure_ascii=False, indent=2))
|
|
|
+ lines.append(json_str)
|
|
|
lines.append("```")
|
|
|
|
|
|
lines.append("")
|
|
|
@@ -470,6 +677,10 @@ class StepTreeDumper:
|
|
|
result[key] = f"<IMAGE_DATA: {data_size_kb:.1f}KB, {header}, preview: {data[:50]}...>"
|
|
|
else:
|
|
|
result[key] = value[:max_length] + f"... ({len(value)} chars)"
|
|
|
+ # 检测 blob 引用
|
|
|
+ elif isinstance(value, str) and value.startswith("blob://"):
|
|
|
+ blob_ref = value.replace("blob://", "")
|
|
|
+ result[key] = f"<BLOB_REF: {blob_ref[:8]}...>"
|
|
|
else:
|
|
|
result[key] = self._truncate_image_data(value, max_length)
|
|
|
return result
|