Coverage for src / harnessutils / conversion / to_model.py: 98%
66 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-04-11 08:10 -0600
« prev ^ index » next coverage.py v7.13.2, created at 2026-04-11 08:10 -0600
1"""Convert internal messages to model format.
3Transforms internal message format (granular, storage-optimized)
4to model format (LLM-compatible).
5"""
7from typing import Any
9from harnessutils.models.message import Message
10from harnessutils.models.parts import ToolPart
13def to_model_messages(
14 messages: list[Message],
15 excluded_outputs: set[str] | None = None,
16) -> list[dict[str, Any]]:
17 """Convert internal messages to model format.
19 Handles:
20 - Excluded tool outputs (replace with marker, via excluded_outputs set)
21 - Legacy compacted tool outputs (replace with marker, via time.compacted)
22 - Interrupted tool calls (inject error)
23 - Two-part system prompts
24 - Stop at summary message
26 Args:
27 messages: Internal message objects
28 excluded_outputs: Call IDs whose outputs should be omitted from the payload
30 Returns:
31 List of messages in model format
32 """
33 excluded = excluded_outputs or set()
34 model_messages: list[dict[str, Any]] = []
35 summary_found = False
36 summary_parent_id: str | None = None
38 for msg in reversed(messages):
39 if summary_found and msg.role == "user" and msg.id == summary_parent_id:
40 break
42 if msg.role == "assistant" and msg.summary:
43 summary_found = True
44 summary_parent_id = msg.parent_id
46 if len(msg.parts) == 0:
47 continue
49 if msg.role == "user":
50 model_msg = _convert_user_message(msg)
51 if model_msg and model_msg.get("content"):
52 model_messages.insert(0, model_msg)
54 elif msg.role == "assistant":
55 model_msg = _convert_assistant_message(msg, excluded_outputs=excluded)
56 if model_msg and model_msg.get("content"):
57 model_messages.insert(0, model_msg)
59 return model_messages
62def _convert_user_message(msg: Message) -> dict[str, Any] | None:
63 """Convert user message to model format.
65 Args:
66 msg: User message
68 Returns:
69 Model format message or None if empty
70 """
71 content_parts: list[str] = []
73 for part in msg.parts:
74 if part.type == "text" and not getattr(part, "ignored", False):
75 content_parts.append(getattr(part, "text", ""))
76 elif part.type == "compaction":
77 content_parts.append("What did we do so far?")
79 if not content_parts:
80 return None
82 return {
83 "role": "user",
84 "content": "\n".join(content_parts),
85 }
88def _convert_assistant_message(
89 msg: Message,
90 excluded_outputs: set[str] | None = None,
91) -> dict[str, Any] | None:
92 """Convert assistant message to model format.
94 Args:
95 msg: Assistant message
96 excluded_outputs: Call IDs whose outputs should be replaced with a marker
98 Returns:
99 Model format message or None if empty
100 """
101 if msg.error and not msg.has_partial_output():
102 return None
104 excluded = excluded_outputs or set()
105 content_parts: list[str] = []
107 for part in msg.parts:
108 if part.type == "text":
109 content_parts.append(getattr(part, "text", ""))
111 elif part.type == "reasoning":
112 content_parts.append(f"[Extended thinking: {len(getattr(part, 'text', ''))} chars]")
114 elif part.type == "tool":
115 tool_part = part
116 assert isinstance(tool_part, ToolPart)
118 if tool_part.state.status == "completed":
119 is_excluded = tool_part.call_id in excluded
120 is_legacy_compacted = bool(
121 tool_part.state.time and tool_part.state.time.compacted
122 )
123 if is_excluded or is_legacy_compacted:
124 if tool_part.state.summary_text:
125 output = f"[Summarized: {tool_part.state.summary_text}]"
126 elif is_legacy_compacted:
127 output = "[Old tool result content cleared]"
128 else:
129 output = "[Tool output excluded from context]"
130 else:
131 output = tool_part.state.output
133 content_parts.append(f"[Tool: {tool_part.tool}] {tool_part.state.title}\n{output}")
135 elif tool_part.state.status == "error":
136 content_parts.append(f"[Tool Error: {tool_part.tool}] {tool_part.state.error}")
138 elif tool_part.state.status in ["pending", "running"]:
139 content_parts.append(f"[Tool execution was interrupted: {tool_part.tool}]")
141 if not content_parts:
142 return None
144 return {
145 "role": "assistant",
146 "content": "\n\n".join(content_parts),
147 }