|
60 | 60 | a synthetic ToolRequestContent Full is emitted before the response. |
61 | 61 | mcp_tool_call -> same as command_execution |
62 | 62 | web_search -> same as command_execution |
63 | | - todo_list -> same as command_execution |
| 63 | + todo_list -> same as command_execution, plus: |
| 64 | + item.updated -> StreamTaskMessageFull(ToolResponseContent) |
| 65 | + Codex ticks one in-place todo_list item through |
| 66 | + item.updated; each revision is republished under |
| 67 | + the same tool_call_id so consumers can render the |
| 68 | + checklist filling in rather than jumping to its |
| 69 | + final state at end of turn. |
64 | 70 | collab_tool_call -> same as command_execution |
65 | 71 | error (item type) -> StreamTaskMessageFull(TextContent, error text) on completed only |
66 | 72 |
|
|
75 | 81 | without this module needing to know about spans. |
76 | 82 | item.updated (reasoning): the intermediate cumulative text is discarded; |
77 | 83 | only item.completed carries the final text. |
78 | | - item.updated (tool): tool item types other than agent_message do not |
79 | | - emit updates; item.started opens the request and |
80 | | - item.completed closes it. |
| 84 | + item.updated (tool): only todo_list is republished (see above). For the |
| 85 | + other tool item types item.started opens the request |
| 86 | + and item.completed closes it; any updates in between |
| 87 | + carry no state a consumer could act on. |
81 | 88 | """ |
82 | 89 |
|
83 | 90 | from __future__ import annotations |
@@ -112,6 +119,11 @@ def _truncate(text: str, max_len: int = _MAX_RESULT_LENGTH) -> str: |
112 | 119 | return str(text)[:max_len] |
113 | 120 |
|
114 | 121 |
|
| 122 | +# Tool items codex revises in place rather than reopening. Their item.updated |
| 123 | +# events carry real intermediate state, so they are forwarded as responses. |
| 124 | +_PROGRESSIVE_TOOL_ITEMS = frozenset({"todo_list"}) |
| 125 | + |
| 126 | + |
115 | 127 | def _tool_name_for(item_type: str, payload: dict[str, Any]) -> str: |
116 | 128 | """Derive a canonical tool name from a codex item type.""" |
117 | 129 | if item_type == "command_execution": |
@@ -467,6 +479,33 @@ def _handle_item(self, evt_type: str, item: dict[str, Any]) -> list[StreamTaskMe |
467 | 479 | ) |
468 | 480 | out.append(StreamTaskMessageDone(type="done", index=req_idx)) |
469 | 481 |
|
| 482 | + elif evt_type == "item.updated" and item_type in _PROGRESSIVE_TOOL_ITEMS: |
| 483 | + # Codex revises its plan in place: one todo_list item is opened |
| 484 | + # at the start of the turn and ticked off through item.updated, |
| 485 | + # with item.completed only arriving at the very end. Forwarding |
| 486 | + # each revision as a response for the SAME tool_call_id lets a |
| 487 | + # consumer show the checklist filling in as the turn runs; the |
| 488 | + # last response received is the current state. |
| 489 | + if item_id in self._tool_open: |
| 490 | + actual_type = self._tool_item_types.get(item_id, item_type) |
| 491 | + result_text, is_error = _tool_output_for(actual_type, item) |
| 492 | + resp_content: dict[str, Any] = {"result": result_text} |
| 493 | + if is_error: |
| 494 | + resp_content["is_error"] = True |
| 495 | + out.append( |
| 496 | + StreamTaskMessageFull( |
| 497 | + type="full", |
| 498 | + index=self._alloc(), |
| 499 | + content=ToolResponseContent( |
| 500 | + type="tool_response", |
| 501 | + author="agent", |
| 502 | + tool_call_id=tool_call_id, |
| 503 | + name=_tool_name_for(actual_type, item), |
| 504 | + content=resp_content, |
| 505 | + ), |
| 506 | + ) |
| 507 | + ) |
| 508 | + |
470 | 509 | elif evt_type == "item.completed": |
471 | 510 | # file_change items may only emit item.completed (no started). |
472 | 511 | if item_id not in self._tool_open: |
|
0 commit comments