|
47 | 47 |
|
48 | 48 | _TOOL_CALL_CONTENT_TYPES: Final[set[str]] = { |
49 | 49 | "function_call", |
| 50 | + "computer_tool_call", |
50 | 51 | "mcp_server_tool_call", |
51 | 52 | "code_interpreter_tool_call", |
52 | 53 | "shell_tool_call", |
@@ -146,20 +147,25 @@ def _is_reasoning_only_assistant(message: Message) -> bool: |
146 | 147 | return all(content.type == "text_reasoning" for content in message.contents) |
147 | 148 |
|
148 | 149 |
|
149 | | -def _unambiguous_function_call_result_pairs(messages: Sequence[Message]) -> list[tuple[int, int]]: |
150 | | - unmatched_declaration_indices: dict[str, list[int]] = {} |
| 150 | +def _unambiguous_tool_call_result_pairs(messages: Sequence[Message]) -> list[tuple[int, int]]: |
| 151 | + unmatched_declaration_indices: dict[tuple[str, str], list[int]] = {} |
151 | 152 | pairs: list[tuple[int, int]] = [] |
152 | 153 |
|
153 | 154 | for message_index, message in enumerate(messages): |
154 | 155 | if message.role not in ("assistant", "tool"): |
155 | 156 | continue |
156 | 157 | for content in message.contents: |
157 | | - if message.role == "assistant" and content.type == "function_call" and content.call_id: |
158 | | - unmatched_declaration_indices.setdefault(content.call_id, []).append(message_index) |
| 158 | + if ( |
| 159 | + message.role == "assistant" |
| 160 | + and content.type in ("function_call", "computer_tool_call") |
| 161 | + and content.call_id |
| 162 | + ): |
| 163 | + unmatched_declaration_indices.setdefault((content.type, content.call_id), []).append(message_index) |
159 | 164 | continue |
160 | | - if content.type != "function_result" or not content.call_id: |
| 165 | + if content.type not in ("function_result", "computer_tool_result") or not content.call_id: |
161 | 166 | continue |
162 | | - candidates = unmatched_declaration_indices.get(content.call_id) |
| 167 | + call_type = "function_call" if content.type == "function_result" else "computer_tool_call" |
| 168 | + candidates = unmatched_declaration_indices.get((call_type, content.call_id)) |
163 | 169 | if candidates is None or len(candidates) != 1: |
164 | 170 | continue |
165 | 171 | pairs.append((candidates.pop(), message_index)) |
@@ -191,8 +197,8 @@ def _group_id_for(message: Message, group_index: int) -> str: |
191 | 197 | return f"group_index_{group_index}" |
192 | 198 |
|
193 | 199 |
|
194 | | -def _link_function_call_result_spans(messages: Sequence[Message], spans: list[dict[str, Any]]) -> None: |
195 | | - """Link non-adjacent function results to unambiguous declaration occurrences.""" |
| 200 | +def _link_tool_call_result_spans(messages: Sequence[Message], spans: list[dict[str, Any]]) -> None: |
| 201 | + """Link non-adjacent tool results to unambiguous declaration occurrences.""" |
196 | 202 | if len(spans) < 2: |
197 | 203 | return |
198 | 204 |
|
@@ -222,7 +228,7 @@ def union(left: int, right: int) -> bool: |
222 | 228 | return True |
223 | 229 |
|
224 | 230 | linked = False |
225 | | - for declaration_message_index, result_message_index in _unambiguous_function_call_result_pairs(messages): |
| 231 | + for declaration_message_index, result_message_index in _unambiguous_tool_call_result_pairs(messages): |
226 | 232 | declaration_span_index = span_by_message_index[declaration_message_index] |
227 | 233 | result_span_index = span_by_message_index[result_message_index] |
228 | 234 | if declaration_span_index < result_span_index and union(result_span_index, declaration_span_index): |
@@ -260,7 +266,7 @@ def group_messages( |
260 | 266 | Returns: |
261 | 267 | Ordered list of lightweight span dicts with keys: |
262 | 268 | ``group_id``, ``kind``, ``start_index``, ``end_index``, ``has_reasoning``. |
263 | | - Non-contiguous function-call declaration and result spans share a group id. |
| 269 | + Non-contiguous function or computer call/result spans share a group id. |
264 | 270 | """ |
265 | 271 | _ensure_message_ids(messages, id_offset=id_offset, reserved_ids=reserved_ids) |
266 | 272 | spans: list[dict[str, Any]] = [] |
@@ -365,7 +371,7 @@ def group_messages( |
365 | 371 | i += 1 |
366 | 372 | group_index += 1 |
367 | 373 |
|
368 | | - _link_function_call_result_spans(messages, spans) |
| 374 | + _link_tool_call_result_spans(messages, spans) |
369 | 375 | return spans |
370 | 376 |
|
371 | 377 |
|
@@ -828,19 +834,24 @@ def _reannotation_start(messages: Sequence[Message], index: int) -> int: |
828 | 834 | return previous_index |
829 | 835 |
|
830 | 836 |
|
831 | | -def _function_pair_reannotation_start(messages: Sequence[Message], start_index: int) -> int: |
832 | | - unmatched_declaration_indices: dict[str, list[int]] = {} |
| 837 | +def _tool_pair_reannotation_start(messages: Sequence[Message], start_index: int) -> int: |
| 838 | + unmatched_declaration_indices: dict[tuple[str, str], list[int]] = {} |
833 | 839 | matching_indices: list[int] = [] |
834 | 840 | for message_index, message in enumerate(messages): |
835 | 841 | if message.role not in ("assistant", "tool"): |
836 | 842 | continue |
837 | 843 | for content in message.contents: |
838 | | - if message.role == "assistant" and content.type == "function_call" and content.call_id: |
839 | | - unmatched_declaration_indices.setdefault(content.call_id, []).append(message_index) |
| 844 | + if ( |
| 845 | + message.role == "assistant" |
| 846 | + and content.type in ("function_call", "computer_tool_call") |
| 847 | + and content.call_id |
| 848 | + ): |
| 849 | + unmatched_declaration_indices.setdefault((content.type, content.call_id), []).append(message_index) |
840 | 850 | continue |
841 | | - if content.type != "function_result" or not content.call_id: |
| 851 | + if content.type not in ("function_result", "computer_tool_result") or not content.call_id: |
842 | 852 | continue |
843 | | - candidates = unmatched_declaration_indices.get(content.call_id) |
| 853 | + call_type = "function_call" if content.type == "function_result" else "computer_tool_call" |
| 854 | + candidates = unmatched_declaration_indices.get((call_type, content.call_id)) |
844 | 855 | if not candidates: |
845 | 856 | continue |
846 | 857 | if message_index >= start_index: |
@@ -894,7 +905,7 @@ def annotate_message_groups( |
894 | 905 | start_index = min(candidate_starts) |
895 | 906 |
|
896 | 907 | start_index = _reannotation_start(messages, start_index) |
897 | | - start_index = _function_pair_reannotation_start(messages, start_index) |
| 908 | + start_index = _tool_pair_reannotation_start(messages, start_index) |
898 | 909 |
|
899 | 910 | # Linked groups can be non-contiguous, so the last prefix message does not |
900 | 911 | # necessarily carry the highest group index. |
@@ -1585,6 +1596,14 @@ def _format_summary_content(content: Content) -> str: |
1585 | 1596 | if content.call_id: |
1586 | 1597 | call += f" [call_id={content.call_id}]" |
1587 | 1598 | return call |
| 1599 | + if content.type == "computer_tool_call": |
| 1600 | + actions = _tool_result_text(content.actions) if content.actions is not None else "no actions" |
| 1601 | + call_id_suffix = f" [call_id={content.call_id}]" if content.call_id else "" |
| 1602 | + return f"computer_tool_call: {actions}{call_id_suffix}" |
| 1603 | + if content.type == "computer_tool_result": |
| 1604 | + call_id_suffix = f" [call_id={content.call_id}]" if content.call_id else "" |
| 1605 | + screenshot_label = ": screenshot" if content.screenshot is not None else "" |
| 1606 | + return f"computer_tool_result{screenshot_label}{call_id_suffix}" |
1588 | 1607 | if content.type == "function_result": |
1589 | 1608 | result_text = _format_summary_result_items(content.items) if content.items else "" |
1590 | 1609 | if not result_text: |
|
0 commit comments