|
29 | 29 | ) |
30 | 30 | from .logger import logger |
31 | 31 | from .run_context import RunContextWrapper |
32 | | -from .run_internal.items import run_item_to_input_item |
| 32 | +from .run_internal.items import run_items_to_input_items |
33 | 33 | from .run_internal.run_steps import ( |
34 | 34 | NextStepInterruption, |
35 | 35 | ProcessedResponse, |
@@ -110,6 +110,40 @@ def _populate_state_from_result( |
110 | 110 | return state |
111 | 111 |
|
112 | 112 |
|
| 113 | +ToInputListMode = Literal["preserve_all", "normalized"] |
| 114 | + |
| 115 | + |
| 116 | +def _input_items_for_result( |
| 117 | + result: RunResultBase, |
| 118 | + *, |
| 119 | + mode: ToInputListMode, |
| 120 | + reasoning_item_id_policy: Literal["preserve", "omit"] | None, |
| 121 | +) -> list[TResponseInputItem]: |
| 122 | + """Return input items for the requested result view. |
| 123 | +
|
| 124 | + ``preserve_all`` keeps the full converted history from ``new_items``. ``normalized`` returns |
| 125 | + the canonical continuation input when handoff filtering rewrote model history, otherwise it |
| 126 | + falls back to the same converted history. |
| 127 | + """ |
| 128 | + session_items = run_items_to_input_items(result.new_items, reasoning_item_id_policy) |
| 129 | + if mode == "preserve_all": |
| 130 | + return session_items |
| 131 | + if mode != "normalized": |
| 132 | + raise ValueError(f"Unsupported to_input_list mode: {mode}") |
| 133 | + if not getattr(result, "_replay_from_model_input_items", False): |
| 134 | + # Most runs never rewrite continuation history, so normalized stays identical to the |
| 135 | + # historical preserve-all view unless the runner explicitly marked a divergence. |
| 136 | + return session_items |
| 137 | + |
| 138 | + model_input_items = getattr(result, "_model_input_items", None) |
| 139 | + if not isinstance(model_input_items, list): |
| 140 | + return session_items |
| 141 | + |
| 142 | + # When the runner marks a divergence, generated_items already reflect the continuation input |
| 143 | + # chosen for the next local run after applying handoff/input filtering. |
| 144 | + return run_items_to_input_items(model_input_items, reasoning_item_id_policy) |
| 145 | + |
| 146 | + |
113 | 147 | @dataclass |
114 | 148 | class RunResultBase(abc.ABC): |
115 | 149 | input: str | list[TResponseInputItem] |
@@ -145,6 +179,12 @@ class RunResultBase(abc.ABC): |
145 | 179 |
|
146 | 180 | _trace_state: TraceState | None = field(default=None, init=False, repr=False) |
147 | 181 | """Serialized trace metadata captured during the run.""" |
| 182 | + _replay_from_model_input_items: bool = field(default=False, init=False, repr=False) |
| 183 | + """Whether replay helpers should prefer `_model_input_items` over `new_items`. |
| 184 | +
|
| 185 | + This is only set when the runner preserved extra session history items that should not be |
| 186 | + replayed into the next local run, such as nested handoff history or filtered handoff input. |
| 187 | + """ |
148 | 188 |
|
149 | 189 | @classmethod |
150 | 190 | def __get_pydantic_core_schema__( |
@@ -208,18 +248,25 @@ def final_output_as(self, cls: type[T], raise_if_incorrect_type: bool = False) - |
208 | 248 |
|
209 | 249 | return cast(T, self.final_output) |
210 | 250 |
|
211 | | - def to_input_list(self) -> list[TResponseInputItem]: |
212 | | - """Creates a new input list, merging the original input with all the new items generated.""" |
| 251 | + def to_input_list( |
| 252 | + self, |
| 253 | + *, |
| 254 | + mode: ToInputListMode = "preserve_all", |
| 255 | + ) -> list[TResponseInputItem]: |
| 256 | + """Create an input-item view of this run. |
| 257 | +
|
| 258 | + ``mode="preserve_all"`` keeps the historical behavior of converting ``new_items`` into a |
| 259 | + full plain-item history. ``mode="normalized"`` prefers the canonical continuation input |
| 260 | + when handoff filtering rewrote model history, while remaining identical for ordinary runs. |
| 261 | + """ |
213 | 262 | original_items: list[TResponseInputItem] = ItemHelpers.input_to_new_input_list(self.input) |
214 | | - new_items: list[TResponseInputItem] = [] |
215 | 263 | reasoning_item_id_policy = getattr(self, "_reasoning_item_id_policy", None) |
216 | | - for item in self.new_items: |
217 | | - converted = run_item_to_input_item(item, reasoning_item_id_policy) |
218 | | - if converted is None: |
219 | | - continue |
220 | | - new_items.append(converted) |
221 | | - |
222 | | - return original_items + new_items |
| 264 | + replay_items = _input_items_for_result( |
| 265 | + self, |
| 266 | + mode=mode, |
| 267 | + reasoning_item_id_policy=reasoning_item_id_policy, |
| 268 | + ) |
| 269 | + return original_items + replay_items |
223 | 270 |
|
224 | 271 | @property |
225 | 272 | def agent_tool_invocation(self) -> AgentToolInvocation | None: |
|
0 commit comments