f28d22984d
* fix(pr-review): properly capture structured output from SDK ResultMessage Fixes critical bug where PR follow-up reviews showed "0 previous findings addressed" despite AI correctly analyzing resolution status. Root Causes Fixed: 1. sdk_utils.py - ResultMessage handling - Added proper check for msg.type == "result" per Anthropic SDK docs - Handle msg.subtype == "success" for structured output capture - Handle error_max_structured_output_retries error case - Added visible logging when structured output is captured 2. parallel_followup_reviewer.py - Silent fallback prevention - Added warning logging when structured output is missing - Added _extract_partial_data() to recover data when Pydantic fails - Prevents complete data loss when schema validation has minor issues 3. parallel_followup_reviewer.py - CI status enforcement - Added code enforcement for failing CI (override to BLOCKED) - Added enforcement for pending CI (downgrade READY_TO_MERGE) - AI prompt compliance is no longer the only safeguard 4. test_dependency_validator.py - macOS compatibility fixes - Fixed symlink comparison issue (/var vs /private/var) - Fixed case-sensitivity comparison for filesystem Impact: Before: AI analysis showed "3/4 resolved" but summary showed "0 resolved" After: Structured output properly captured, fallback extraction if needed Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(pr-review): rescan related files after worktree creation Related files were always returning 0 because context gathering happened BEFORE the worktree was created. For fork PRs or PRs with new files, the files don't exist in the local checkout, so the related files lookup failed. This fix: - Adds `find_related_files_for_root()` static method to ContextGatherer that can search for related files using any project root path - Restructures ParallelOrchestratorReviewer.review() to create the worktree FIRST, then rescan for related files using the worktree path, then build the prompt with the updated context Now the PR review will correctly find related test files, config files, and type definitions that exist in the PR. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(pr-review): add visible logging for worktree creation and rescan Add always-visible logs (not gated by DEBUG_MODE) to show: - When worktree is created for PR review - Result of related files rescan in worktree This helps verify the fix is working and diagnose issues. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(pr-review): show model name when invoking specialist agents Add model information to agent invocation logs so users can see which model each agent is using. This helps with debugging and monitoring. Example log output: [ParallelOrchestrator] Invoking agent: logic-reviewer [sonnet-4.5] [ParallelOrchestrator] Invoking agent: quality-reviewer [sonnet-4.5] Added _short_model_name() helper to convert full model names like "claude-sonnet-4-5-20250929" to short display names like "sonnet-4.5". Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(sdk-utils): add model info to AssistantMessage tool invocation logs The agent invocation log was missing the model info when tool calls came through AssistantMessage content blocks (vs standalone ToolUseBlock). Now both code paths show the model name consistently. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(sdk-utils): add user-visible progress and activity logging Previously, most SDK stream activity was hidden behind DEBUG_MODE, making it hard for users to see what's happening during PR reviews. Changes: - Add periodic progress logs every 10 messages showing agent count - Show tool usage (Read, Grep, etc.) not just Task calls - Show tool completion results with brief preview - Model info now shown for all agent invocation paths Users will now see: - "[ParallelOrchestrator] Processing... (20 messages, 4 agents working)" - "[ParallelOrchestrator] Using tool: Read" - "[ParallelOrchestrator] Tool result [done]: ..." - "[ParallelOrchestrator] Invoking agent: logic-reviewer [opus-4.5]" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(pr-review): improve worktree visibility and fix log categorization 1. Frontend log categorization: - Add "PRReview" and "ClientCache" to analysisSources - [PRReview] logs now appear in "AI Analysis" section instead of "Synthesis" 2. Enhanced worktree logging: - Show file count in worktree creation log - Display PR branch HEAD SHA for verification - Format: "[PRReview] Created temporary worktree: pr-xxx (1,234 files)" 3. Structured output detection: - Also check for msg_type == "ResultMessage" (SDK class name) - Add diagnostic logging in DEBUG mode to trace ResultMessage handling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore(deps): update claude-agent-sdk to >=0.1.19 Update to latest SDK version for structured output improvements. Previous: >=0.1.16 Latest available: 0.1.19 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(sdk-utils): consolidate structured output capture to single location BREAKING: Simplified structured output handling to follow official Python SDK pattern. Before: 5 different capture locations causing "Multiple StructuredOutput blocks" warnings After: 1 capture location using hasattr(msg, 'structured_output') per official docs Changes: - Remove 4 redundant capture paths (ToolUseBlock, AssistantMessage content, legacy, ResultMessage) - Single capture point: if hasattr(msg, 'structured_output') and msg.structured_output - Skip duplicates silently (only capture first one) - Keep error handling for error_max_structured_output_retries - Skip logging StructuredOutput tool calls (handled separately) - Cleaner, more maintainable code following official SDK pattern Reference: https://platform.claude.com/docs/en/agent-sdk/structured-outputs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(pr-logs): enhance log visibility and organization for agent activities - Introduced a new logging structure to categorize agent logs into groups, improving readability and user experience. - Added functionality to toggle visibility of agent logs and orchestrator tool activities, allowing users to focus on relevant information. - Implemented helper functions to identify tool activity logs and group entries by agent, enhancing log organization. - Updated UI components to support the new log grouping and toggling features, ensuring a seamless user interface. This update aims to provide clearer insights into agent activities during PR reviews, making it easier for users to track progress and actions taken by agents. * fix(pr-review): address PR review findings for reliability and UX - Fix CI pending check asymmetry: check MERGE_WITH_CHANGES verdict - Add file count limit (10k) to prevent slow rglob on large repos - Extract CONFIG_FILE_NAMES constant to fix DRY violation - Fix misleading "agents working" count by tracking completed agents - Add i18n translations for agent activity logs (en/fr) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(pr-logs): categorize Followup logs to context phase for follow-up reviews The Followup source logs context gathering work (comparing commits, finding changed files, gathering feedback) not analysis. Move from analysisSources to contextSources so follow-up review logs appear in the correct phase. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
476 lines
21 KiB
Python
476 lines
21 KiB
Python
"""
|
|
SDK Stream Processing Utilities
|
|
================================
|
|
|
|
Shared utilities for processing Claude Agent SDK response streams.
|
|
|
|
This module extracts common SDK message processing patterns used across
|
|
parallel orchestrator and follow-up reviewers.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
try:
|
|
from .io_utils import safe_print
|
|
except (ImportError, ValueError, SystemError):
|
|
from core.io_utils import safe_print
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Check if debug mode is enabled
|
|
DEBUG_MODE = os.environ.get("DEBUG", "").lower() in ("true", "1", "yes")
|
|
|
|
|
|
def _short_model_name(model: str | None) -> str:
|
|
"""Convert full model name to a short display name for logs.
|
|
|
|
Examples:
|
|
claude-sonnet-4-5-20250929 -> sonnet-4.5
|
|
claude-opus-4-5-20251101 -> opus-4.5
|
|
claude-3-5-sonnet-20241022 -> sonnet-3.5
|
|
"""
|
|
if not model:
|
|
return "unknown"
|
|
|
|
model_lower = model.lower()
|
|
|
|
# Handle new model naming (claude-{model}-{version}-{date})
|
|
if "opus-4-5" in model_lower or "opus-4.5" in model_lower:
|
|
return "opus-4.5"
|
|
if "sonnet-4-5" in model_lower or "sonnet-4.5" in model_lower:
|
|
return "sonnet-4.5"
|
|
if "haiku-4" in model_lower:
|
|
return "haiku-4"
|
|
|
|
# Handle older model naming (claude-3-5-{model})
|
|
if "3-5-sonnet" in model_lower or "3.5-sonnet" in model_lower:
|
|
return "sonnet-3.5"
|
|
if "3-5-haiku" in model_lower or "3.5-haiku" in model_lower:
|
|
return "haiku-3.5"
|
|
if "3-opus" in model_lower:
|
|
return "opus-3"
|
|
if "3-sonnet" in model_lower:
|
|
return "sonnet-3"
|
|
if "3-haiku" in model_lower:
|
|
return "haiku-3"
|
|
|
|
# Fallback: return last part before date (if matches pattern)
|
|
parts = model.split("-")
|
|
if len(parts) >= 2:
|
|
# Try to find model type (opus, sonnet, haiku)
|
|
for i, part in enumerate(parts):
|
|
if part.lower() in ("opus", "sonnet", "haiku"):
|
|
return part.lower()
|
|
|
|
return model[:20] # Truncate if nothing else works
|
|
|
|
|
|
def _get_tool_detail(tool_name: str, tool_input: dict[str, Any]) -> str:
|
|
"""Extract meaningful detail from tool input for user-friendly logging.
|
|
|
|
Instead of "Using tool: Read", show "Reading sdk_utils.py"
|
|
Instead of "Using tool: Grep", show "Searching for 'pattern'"
|
|
"""
|
|
if tool_name == "Read":
|
|
file_path = tool_input.get("file_path", "")
|
|
if file_path:
|
|
# Extract just the filename for brevity
|
|
filename = file_path.split("/")[-1] if "/" in file_path else file_path
|
|
return f"Reading {filename}"
|
|
return "Reading file"
|
|
|
|
if tool_name == "Grep":
|
|
pattern = tool_input.get("pattern", "")
|
|
if pattern:
|
|
# Truncate long patterns
|
|
pattern_preview = pattern[:40] + "..." if len(pattern) > 40 else pattern
|
|
return f"Searching for '{pattern_preview}'"
|
|
return "Searching codebase"
|
|
|
|
if tool_name == "Glob":
|
|
pattern = tool_input.get("pattern", "")
|
|
if pattern:
|
|
return f"Finding files matching '{pattern}'"
|
|
return "Finding files"
|
|
|
|
if tool_name == "Bash":
|
|
command = tool_input.get("command", "")
|
|
if command:
|
|
# Show first part of command
|
|
cmd_preview = command[:50] + "..." if len(command) > 50 else command
|
|
return f"Running: {cmd_preview}"
|
|
return "Running command"
|
|
|
|
if tool_name == "Edit":
|
|
file_path = tool_input.get("file_path", "")
|
|
if file_path:
|
|
filename = file_path.split("/")[-1] if "/" in file_path else file_path
|
|
return f"Editing {filename}"
|
|
return "Editing file"
|
|
|
|
if tool_name == "Write":
|
|
file_path = tool_input.get("file_path", "")
|
|
if file_path:
|
|
filename = file_path.split("/")[-1] if "/" in file_path else file_path
|
|
return f"Writing {filename}"
|
|
return "Writing file"
|
|
|
|
# Default fallback for unknown tools
|
|
return f"Using tool: {tool_name}"
|
|
|
|
|
|
async def process_sdk_stream(
|
|
client: Any,
|
|
on_thinking: Callable[[str], None] | None = None,
|
|
on_tool_use: Callable[[str, str, dict[str, Any]], None] | None = None,
|
|
on_tool_result: Callable[[str, bool, Any], None] | None = None,
|
|
on_text: Callable[[str], None] | None = None,
|
|
on_structured_output: Callable[[dict[str, Any]], None] | None = None,
|
|
context_name: str = "SDK",
|
|
model: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Process SDK response stream with customizable callbacks.
|
|
|
|
This function handles the common pattern of:
|
|
- Tracking thinking blocks
|
|
- Tracking tool invocations (especially Task/subagent calls)
|
|
- Tracking tool results
|
|
- Collecting text output
|
|
- Extracting structured output (per official Python SDK pattern)
|
|
|
|
Args:
|
|
client: Claude SDK client with receive_response() method
|
|
on_thinking: Callback for thinking blocks - receives thinking text
|
|
on_tool_use: Callback for tool invocations - receives (tool_name, tool_id, tool_input)
|
|
on_tool_result: Callback for tool results - receives (tool_id, is_error, result_content)
|
|
on_text: Callback for text output - receives text string
|
|
on_structured_output: Callback for structured output - receives dict
|
|
context_name: Name for logging (e.g., "ParallelOrchestrator", "ParallelFollowup")
|
|
model: Model name for logging (e.g., "claude-sonnet-4-5-20250929")
|
|
|
|
Returns:
|
|
Dictionary with:
|
|
- result_text: Accumulated text output
|
|
- structured_output: Final structured output (if any)
|
|
- agents_invoked: List of agent names invoked via Task tool
|
|
- msg_count: Total message count
|
|
- subagent_tool_ids: Mapping of tool_id -> agent_name
|
|
- error: Error message if stream processing failed (None on success)
|
|
"""
|
|
result_text = ""
|
|
structured_output = None
|
|
agents_invoked = []
|
|
msg_count = 0
|
|
stream_error = None
|
|
# Track subagent tool IDs to log their results
|
|
subagent_tool_ids: dict[str, str] = {} # tool_id -> agent_name
|
|
completed_agent_tool_ids: set[str] = set() # tool_ids of completed agents
|
|
|
|
safe_print(f"[{context_name}] Processing SDK stream...")
|
|
if DEBUG_MODE:
|
|
safe_print(f"[DEBUG {context_name}] Awaiting response stream...")
|
|
|
|
# Track activity for progress logging
|
|
last_progress_log = 0
|
|
PROGRESS_LOG_INTERVAL = 10 # Log progress every N messages
|
|
|
|
try:
|
|
async for msg in client.receive_response():
|
|
try:
|
|
msg_type = type(msg).__name__
|
|
msg_count += 1
|
|
|
|
# Log progress periodically so user knows AI is working
|
|
if msg_count - last_progress_log >= PROGRESS_LOG_INTERVAL:
|
|
if subagent_tool_ids:
|
|
pending = len(subagent_tool_ids) - len(completed_agent_tool_ids)
|
|
if pending > 0:
|
|
safe_print(
|
|
f"[{context_name}] Processing... ({msg_count} messages, {pending} agent{'s' if pending > 1 else ''} working)"
|
|
)
|
|
else:
|
|
safe_print(
|
|
f"[{context_name}] Processing... ({msg_count} messages)"
|
|
)
|
|
else:
|
|
safe_print(
|
|
f"[{context_name}] Processing... ({msg_count} messages)"
|
|
)
|
|
last_progress_log = msg_count
|
|
|
|
if DEBUG_MODE:
|
|
# Log every message type for visibility
|
|
msg_details = ""
|
|
if hasattr(msg, "type"):
|
|
msg_details = f" (type={msg.type})"
|
|
safe_print(
|
|
f"[DEBUG {context_name}] Message #{msg_count}: {msg_type}{msg_details}"
|
|
)
|
|
|
|
# Track thinking blocks
|
|
if msg_type == "ThinkingBlock" or (
|
|
hasattr(msg, "type") and msg.type == "thinking"
|
|
):
|
|
thinking_text = getattr(msg, "thinking", "") or getattr(
|
|
msg, "text", ""
|
|
)
|
|
if thinking_text:
|
|
safe_print(
|
|
f"[{context_name}] AI thinking: {len(thinking_text)} chars"
|
|
)
|
|
if DEBUG_MODE:
|
|
# Show first 200 chars of thinking
|
|
preview = thinking_text[:200].replace("\n", " ")
|
|
safe_print(
|
|
f"[DEBUG {context_name}] Thinking preview: {preview}..."
|
|
)
|
|
# Invoke callback
|
|
if on_thinking:
|
|
on_thinking(thinking_text)
|
|
|
|
# Track subagent invocations (Task tool calls)
|
|
if msg_type == "ToolUseBlock" or (
|
|
hasattr(msg, "type") and msg.type == "tool_use"
|
|
):
|
|
tool_name = getattr(msg, "name", "")
|
|
tool_id = getattr(msg, "id", "unknown")
|
|
tool_input = getattr(msg, "input", {})
|
|
|
|
if DEBUG_MODE:
|
|
safe_print(
|
|
f"[DEBUG {context_name}] Tool call: {tool_name} (id={tool_id})"
|
|
)
|
|
|
|
if tool_name == "Task":
|
|
# Extract which agent was invoked
|
|
agent_name = tool_input.get("subagent_type", "unknown")
|
|
agents_invoked.append(agent_name)
|
|
# Track this tool ID to log its result later
|
|
subagent_tool_ids[tool_id] = agent_name
|
|
# Log with model info if available
|
|
model_info = f" [{_short_model_name(model)}]" if model else ""
|
|
safe_print(
|
|
f"[{context_name}] Invoking agent: {agent_name}{model_info}"
|
|
)
|
|
elif tool_name != "StructuredOutput":
|
|
# Log meaningful tool info (not just tool name)
|
|
tool_detail = _get_tool_detail(tool_name, tool_input)
|
|
safe_print(f"[{context_name}] {tool_detail}")
|
|
|
|
# Invoke callback for all tool uses
|
|
if on_tool_use:
|
|
on_tool_use(tool_name, tool_id, tool_input)
|
|
|
|
# Track tool results
|
|
if msg_type == "ToolResultBlock" or (
|
|
hasattr(msg, "type") and msg.type == "tool_result"
|
|
):
|
|
tool_id = getattr(msg, "tool_use_id", "unknown")
|
|
is_error = getattr(msg, "is_error", False)
|
|
result_content = getattr(msg, "content", "")
|
|
|
|
# Handle list of content blocks
|
|
if isinstance(result_content, list):
|
|
result_content = " ".join(
|
|
str(getattr(c, "text", c)) for c in result_content
|
|
)
|
|
|
|
# Check if this is a subagent result
|
|
if tool_id in subagent_tool_ids:
|
|
agent_name = subagent_tool_ids[tool_id]
|
|
completed_agent_tool_ids.add(tool_id) # Mark agent as completed
|
|
status = "ERROR" if is_error else "complete"
|
|
result_preview = (
|
|
str(result_content)[:600].replace("\n", " ").strip()
|
|
)
|
|
safe_print(
|
|
f"[Agent:{agent_name}] {status}: {result_preview}{'...' if len(str(result_content)) > 600 else ''}"
|
|
)
|
|
else:
|
|
# Show tool completion for visibility (not gated by DEBUG)
|
|
status = "ERROR" if is_error else "done"
|
|
# Show brief preview of result for context
|
|
result_preview = (
|
|
str(result_content)[:100].replace("\n", " ").strip()
|
|
)
|
|
if result_preview:
|
|
safe_print(
|
|
f"[{context_name}] Tool result [{status}]: {result_preview}{'...' if len(str(result_content)) > 100 else ''}"
|
|
)
|
|
|
|
# Invoke callback
|
|
if on_tool_result:
|
|
on_tool_result(tool_id, is_error, result_content)
|
|
|
|
# Collect text output and check for tool uses in content blocks
|
|
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
|
for block in msg.content:
|
|
block_type = type(block).__name__
|
|
|
|
# Check for tool use blocks within content
|
|
if (
|
|
block_type == "ToolUseBlock"
|
|
or getattr(block, "type", "") == "tool_use"
|
|
):
|
|
tool_name = getattr(block, "name", "")
|
|
tool_id = getattr(block, "id", "unknown")
|
|
tool_input = getattr(block, "input", {})
|
|
|
|
if tool_name == "Task":
|
|
agent_name = tool_input.get("subagent_type", "unknown")
|
|
if agent_name not in agents_invoked:
|
|
agents_invoked.append(agent_name)
|
|
subagent_tool_ids[tool_id] = agent_name
|
|
# Log with model info if available
|
|
model_info = (
|
|
f" [{_short_model_name(model)}]"
|
|
if model
|
|
else ""
|
|
)
|
|
safe_print(
|
|
f"[{context_name}] Invoking agent: {agent_name}{model_info}"
|
|
)
|
|
elif tool_name != "StructuredOutput":
|
|
# Log meaningful tool info (not just tool name)
|
|
tool_detail = _get_tool_detail(tool_name, tool_input)
|
|
safe_print(f"[{context_name}] {tool_detail}")
|
|
|
|
# Invoke callback
|
|
if on_tool_use:
|
|
on_tool_use(tool_name, tool_id, tool_input)
|
|
|
|
# Collect text - must check block type since only TextBlock has .text
|
|
block_type = type(block).__name__
|
|
if block_type == "TextBlock" and hasattr(block, "text"):
|
|
result_text += block.text
|
|
# Always print text content preview (not just in DEBUG_MODE)
|
|
text_preview = block.text[:500].replace("\n", " ").strip()
|
|
if text_preview:
|
|
safe_print(
|
|
f"[{context_name}] AI response: {text_preview}{'...' if len(block.text) > 500 else ''}"
|
|
)
|
|
# Invoke callback
|
|
if on_text:
|
|
on_text(block.text)
|
|
|
|
# ================================================================
|
|
# STRUCTURED OUTPUT CAPTURE (Single, consolidated location)
|
|
# Per official Python SDK docs: https://platform.claude.com/docs/en/agent-sdk/structured-outputs
|
|
# The Python pattern is: if hasattr(message, 'structured_output')
|
|
# ================================================================
|
|
|
|
# Check for error_max_structured_output_retries first (SDK validation failed)
|
|
is_result_msg = msg_type == "ResultMessage" or (
|
|
hasattr(msg, "type") and msg.type == "result"
|
|
)
|
|
if is_result_msg:
|
|
subtype = getattr(msg, "subtype", None)
|
|
if DEBUG_MODE:
|
|
safe_print(
|
|
f"[DEBUG {context_name}] ResultMessage: subtype={subtype}"
|
|
)
|
|
if subtype == "error_max_structured_output_retries":
|
|
# SDK failed to produce valid structured output after retries
|
|
logger.warning(
|
|
f"[{context_name}] Claude could not produce valid structured output "
|
|
f"after maximum retries - schema validation failed"
|
|
)
|
|
safe_print(
|
|
f"[{context_name}] WARNING: Structured output validation failed after retries"
|
|
)
|
|
if not stream_error:
|
|
stream_error = "structured_output_validation_failed"
|
|
|
|
# Capture structured output from ANY message that has it
|
|
# This is the official Python SDK pattern - check hasattr()
|
|
if hasattr(msg, "structured_output") and msg.structured_output:
|
|
# Only capture if we don't already have it (avoid duplicates)
|
|
if structured_output is None:
|
|
structured_output = msg.structured_output
|
|
safe_print(f"[{context_name}] Received structured output")
|
|
if on_structured_output:
|
|
on_structured_output(msg.structured_output)
|
|
elif DEBUG_MODE:
|
|
# In debug mode, note that we skipped a duplicate
|
|
safe_print(
|
|
f"[DEBUG {context_name}] Skipping duplicate structured output"
|
|
)
|
|
|
|
# Check for tool results in UserMessage (subagent results come back here)
|
|
if msg_type == "UserMessage" and hasattr(msg, "content"):
|
|
for block in msg.content:
|
|
block_type = type(block).__name__
|
|
# Check for tool result blocks
|
|
if (
|
|
block_type == "ToolResultBlock"
|
|
or getattr(block, "type", "") == "tool_result"
|
|
):
|
|
tool_id = getattr(block, "tool_use_id", "unknown")
|
|
is_error = getattr(block, "is_error", False)
|
|
result_content = getattr(block, "content", "")
|
|
|
|
# Handle list of content blocks
|
|
if isinstance(result_content, list):
|
|
result_content = " ".join(
|
|
str(getattr(c, "text", c)) for c in result_content
|
|
)
|
|
|
|
# Check if this is a subagent result
|
|
if tool_id in subagent_tool_ids:
|
|
agent_name = subagent_tool_ids[tool_id]
|
|
completed_agent_tool_ids.add(
|
|
tool_id
|
|
) # Mark agent as completed
|
|
status = "ERROR" if is_error else "complete"
|
|
result_preview = (
|
|
str(result_content)[:600].replace("\n", " ").strip()
|
|
)
|
|
safe_print(
|
|
f"[Agent:{agent_name}] {status}: {result_preview}{'...' if len(str(result_content)) > 600 else ''}"
|
|
)
|
|
|
|
# Invoke callback
|
|
if on_tool_result:
|
|
on_tool_result(tool_id, is_error, result_content)
|
|
|
|
except (AttributeError, TypeError, KeyError) as msg_error:
|
|
# Log individual message processing errors but continue
|
|
logger.warning(
|
|
f"[{context_name}] Error processing message #{msg_count}: {msg_error}"
|
|
)
|
|
if DEBUG_MODE:
|
|
safe_print(
|
|
f"[DEBUG {context_name}] Message processing error: {msg_error}"
|
|
)
|
|
# Continue processing subsequent messages
|
|
|
|
except BrokenPipeError:
|
|
# Pipe closed by parent process - expected during shutdown
|
|
stream_error = "Output pipe closed"
|
|
logger.debug(f"[{context_name}] Output pipe closed by parent process")
|
|
except Exception as e:
|
|
# Log stream-level errors
|
|
stream_error = str(e)
|
|
logger.error(f"[{context_name}] SDK stream processing failed: {e}")
|
|
safe_print(f"[{context_name}] ERROR: Stream processing failed: {e}")
|
|
|
|
if DEBUG_MODE:
|
|
safe_print(f"[DEBUG {context_name}] Session ended. Total messages: {msg_count}")
|
|
|
|
safe_print(f"[{context_name}] Session ended. Total messages: {msg_count}")
|
|
|
|
return {
|
|
"result_text": result_text,
|
|
"structured_output": structured_output,
|
|
"agents_invoked": agents_invoked,
|
|
"msg_count": msg_count,
|
|
"subagent_tool_ids": subagent_tool_ids,
|
|
"error": stream_error,
|
|
}
|