Skip to content

Commit 74e8c1e

Browse files
authored
fix: persist reasoning item ID policy across resumes and streamed follow-up turns (#2512)
1 parent 3589b6b commit 74e8c1e

17 files changed

+756
-48
lines changed

src/agents/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,13 @@
8686
from .prompts import DynamicPromptFunction, GenerateDynamicPromptData, Prompt
8787
from .repl import run_demo_loop
8888
from .result import RunResult, RunResultStreaming
89-
from .run import RunConfig, Runner, ToolErrorFormatter, ToolErrorFormatterArgs
89+
from .run import (
90+
ReasoningItemIdPolicy,
91+
RunConfig,
92+
Runner,
93+
ToolErrorFormatter,
94+
ToolErrorFormatterArgs,
95+
)
9096
from .run_context import AgentHookContext, RunContextWrapper, TContext
9197
from .run_error_handlers import (
9298
RunErrorData,
@@ -345,6 +351,7 @@ def enable_verbose_stdout_logging():
345351
"RunResult",
346352
"RunResultStreaming",
347353
"RunConfig",
354+
"ReasoningItemIdPolicy",
348355
"ToolErrorFormatter",
349356
"ToolErrorFormatterArgs",
350357
"RunState",

src/agents/result.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
)
3030
from .logger import logger
3131
from .run_context import RunContextWrapper
32+
from .run_internal.items import run_item_to_input_item
3233
from .run_internal.run_steps import (
3334
NextStepInterruption,
3435
ProcessedResponse,
@@ -78,6 +79,7 @@ def _populate_state_from_result(
7879
state._conversation_id = conversation_id
7980
state._previous_response_id = previous_response_id
8081
state._auto_previous_response_id = auto_previous_response_id
82+
state._reasoning_item_id_policy = getattr(result, "_reasoning_item_id_policy", None)
8183

8284
interruptions = list(getattr(result, "interruptions", []))
8385
if interruptions:
@@ -193,10 +195,12 @@ def to_input_list(self) -> list[TResponseInputItem]:
193195
"""Creates a new input list, merging the original input with all the new items generated."""
194196
original_items: list[TResponseInputItem] = ItemHelpers.input_to_new_input_list(self.input)
195197
new_items: list[TResponseInputItem] = []
198+
reasoning_item_id_policy = getattr(self, "_reasoning_item_id_policy", None)
196199
for item in self.new_items:
197-
if isinstance(item, ToolApprovalItem):
200+
converted = run_item_to_input_item(item, reasoning_item_id_policy)
201+
if converted is None:
198202
continue
199-
new_items.append(item.to_input_item())
203+
new_items.append(converted)
200204

201205
return original_items + new_items
202206

@@ -237,6 +241,10 @@ class RunResult(RunResultBase):
237241
"""Response identifier returned by the server for the last turn."""
238242
_auto_previous_response_id: bool = field(default=False, repr=False)
239243
"""Whether automatic previous response tracking was enabled."""
244+
_reasoning_item_id_policy: Literal["preserve", "omit"] | None = field(
245+
default=None, init=False, repr=False
246+
)
247+
"""How reasoning IDs should be represented when converting to input history."""
240248
max_turns: int = 10
241249
"""The maximum number of turns allowed for this run."""
242250
interruptions: list[ToolApprovalItem] = field(default_factory=list)
@@ -399,6 +407,10 @@ class RunResultStreaming(RunResultBase):
399407
"""Response identifier returned by the server for the last turn."""
400408
_auto_previous_response_id: bool = field(default=False, repr=False)
401409
"""Whether automatic previous response tracking was enabled."""
410+
_reasoning_item_id_policy: Literal["preserve", "omit"] | None = field(
411+
default=None, init=False, repr=False
412+
)
413+
"""How reasoning IDs should be represented when converting to input history."""
402414
_run_impl_task: InitVar[asyncio.Task[Any] | None] = None
403415

404416
def __post_init__(self, _run_impl_task: asyncio.Task[Any] | None) -> None:

src/agents/run.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
CallModelData,
3535
CallModelInputFilter,
3636
ModelInputData,
37+
ReasoningItemIdPolicy,
3738
RunConfig,
3839
RunOptions,
3940
ToolErrorFormatter,
@@ -124,6 +125,7 @@
124125
"ModelInputData",
125126
"CallModelData",
126127
"CallModelInputFilter",
128+
"ReasoningItemIdPolicy",
127129
"ToolErrorFormatter",
128130
"ToolErrorFormatterArgs",
129131
"DEFAULT_MAX_TURNS",
@@ -490,6 +492,14 @@ async def run(
490492
)
491493
original_input_for_state = prepared_input
492494

495+
resolved_reasoning_item_id_policy: ReasoningItemIdPolicy | None = (
496+
run_config.reasoning_item_id_policy
497+
if run_config.reasoning_item_id_policy is not None
498+
else (run_state._reasoning_item_id_policy if run_state is not None else None)
499+
)
500+
if run_state is not None:
501+
run_state._reasoning_item_id_policy = resolved_reasoning_item_id_policy
502+
493503
# Check whether to enable OpenAI server-managed conversation
494504
if (
495505
conversation_id is not None
@@ -500,6 +510,7 @@ async def run(
500510
conversation_id=conversation_id,
501511
previous_response_id=previous_response_id,
502512
auto_previous_response_id=auto_previous_response_id,
513+
reasoning_item_id_policy=resolved_reasoning_item_id_policy,
503514
)
504515
else:
505516
server_conversation_tracker = None
@@ -566,8 +577,15 @@ async def run(
566577
previous_response_id=previous_response_id,
567578
auto_previous_response_id=auto_previous_response_id,
568579
)
580+
run_state._reasoning_item_id_policy = resolved_reasoning_item_id_policy
569581
run_state.set_trace(get_current_trace())
570582

583+
def _with_reasoning_item_id_policy(result: RunResult) -> RunResult:
584+
result._reasoning_item_id_policy = resolved_reasoning_item_id_policy
585+
if run_state is not None:
586+
run_state._reasoning_item_id_policy = resolved_reasoning_item_id_policy
587+
return result
588+
571589
pending_server_items: list[RunItem] | None = None
572590
input_guardrail_results: list[InputGuardrailResult] = (
573591
list(run_state._input_guardrail_results) if run_state is not None else []
@@ -675,6 +693,9 @@ async def run(
675693
run_state._current_turn_persisted_item_count
676694
),
677695
response_id=turn_result.model_response.response_id,
696+
reasoning_item_id_policy=(
697+
run_state._reasoning_item_id_policy
698+
),
678699
store=store_setting,
679700
)
680701
)
@@ -727,7 +748,7 @@ async def run(
727748
original_input=original_input,
728749
)
729750
return finalize_conversation_tracking(
730-
result,
751+
_with_reasoning_item_id_policy(result),
731752
server_conversation_tracker=server_conversation_tracker,
732753
run_state=run_state,
733754
)
@@ -792,7 +813,7 @@ async def run(
792813
)
793814
result._original_input = copy_input_items(original_input)
794815
return finalize_conversation_tracking(
795-
result,
816+
_with_reasoning_item_id_policy(result),
796817
server_conversation_tracker=server_conversation_tracker,
797818
run_state=run_state,
798819
)
@@ -852,6 +873,7 @@ async def run(
852873
new_items=session_items,
853874
raw_responses=model_responses,
854875
last_agent=current_agent,
876+
reasoning_item_id_policy=resolved_reasoning_item_id_policy,
855877
)
856878
handler_result = await resolve_run_error_handler_result(
857879
error_handlers=error_handlers,
@@ -921,7 +943,7 @@ async def run(
921943
)
922944
result._original_input = copy_input_items(original_input)
923945
return finalize_conversation_tracking(
924-
result,
946+
_with_reasoning_item_id_policy(result),
925947
server_conversation_tracker=server_conversation_tracker,
926948
run_state=run_state,
927949
)
@@ -995,6 +1017,7 @@ async def run(
9951017
if not is_resumed_state and session_persistence_enabled
9961018
else None
9971019
),
1020+
reasoning_item_id_policy=resolved_reasoning_item_id_policy,
9981021
)
9991022
)
10001023

@@ -1048,6 +1071,7 @@ async def run(
10481071
if not is_resumed_state and session_persistence_enabled
10491072
else None
10501073
),
1074+
reasoning_item_id_policy=resolved_reasoning_item_id_policy,
10511075
)
10521076

10531077
# Start hooks should only run on the first turn unless reset by a handoff.
@@ -1116,6 +1140,9 @@ async def run(
11161140
items_to_save_turn,
11171141
None,
11181142
response_id=turn_result.model_response.response_id,
1143+
reasoning_item_id_policy=(
1144+
run_state._reasoning_item_id_policy
1145+
),
11191146
store=store_setting,
11201147
)
11211148
run_state._current_turn_persisted_item_count += saved_count
@@ -1181,7 +1208,7 @@ async def run(
11811208
)
11821209
result._original_input = copy_input_items(original_input)
11831210
return finalize_conversation_tracking(
1184-
result,
1211+
_with_reasoning_item_id_policy(result),
11851212
server_conversation_tracker=server_conversation_tracker,
11861213
run_state=run_state,
11871214
)
@@ -1242,7 +1269,7 @@ async def run(
12421269
original_input=original_input,
12431270
)
12441271
return finalize_conversation_tracking(
1245-
result,
1272+
_with_reasoning_item_id_policy(result),
12461273
server_conversation_tracker=server_conversation_tracker,
12471274
run_state=run_state,
12481275
)
@@ -1473,6 +1500,14 @@ def run_streamed(
14731500
auto_previous_response_id=auto_previous_response_id,
14741501
)
14751502

1503+
resolved_reasoning_item_id_policy: ReasoningItemIdPolicy | None = (
1504+
run_config.reasoning_item_id_policy
1505+
if run_config.reasoning_item_id_policy is not None
1506+
else (run_state._reasoning_item_id_policy if run_state is not None else None)
1507+
)
1508+
if run_state is not None:
1509+
run_state._reasoning_item_id_policy = resolved_reasoning_item_id_policy
1510+
14761511
(
14771512
trace_workflow_name,
14781513
trace_id,
@@ -1551,6 +1586,7 @@ def run_streamed(
15511586
streamed_result._model_input_items = (
15521587
list(run_state._generated_items) if run_state is not None else []
15531588
)
1589+
streamed_result._reasoning_item_id_policy = resolved_reasoning_item_id_policy
15541590
if run_state is not None:
15551591
streamed_result._trace_state = run_state._trace_state
15561592
# Store run_state in streamed_result._state so it's accessible throughout streaming

src/agents/run_config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class CallModelData(Generic[TContext]):
5151

5252

5353
CallModelInputFilter = Callable[[CallModelData[Any]], MaybeAwaitable[ModelInputData]]
54+
ReasoningItemIdPolicy = Literal["preserve", "omit"]
5455

5556

5657
@dataclass
@@ -183,6 +184,13 @@ class RunConfig:
183184
settings. Used to control session behavior like the number of items to retrieve.
184185
"""
185186

187+
reasoning_item_id_policy: ReasoningItemIdPolicy | None = None
188+
"""Controls how reasoning items are converted to next-turn model input.
189+
190+
- ``None`` / ``"preserve"`` keeps reasoning item IDs as-is.
191+
- ``"omit"`` strips reasoning item IDs from model input built by the runner.
192+
"""
193+
186194

187195
class RunOptions(TypedDict, Generic[TContext]):
188196
"""Arguments for ``AgentRunner`` methods."""
@@ -220,6 +228,7 @@ class RunOptions(TypedDict, Generic[TContext]):
220228
"CallModelData",
221229
"CallModelInputFilter",
222230
"ModelInputData",
231+
"ReasoningItemIdPolicy",
223232
"RunConfig",
224233
"RunOptions",
225234
"ToolErrorFormatter",

src/agents/run_internal/approvals.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from ..agent import Agent
1515
from ..items import ItemHelpers, RunItem, ToolApprovalItem, ToolCallOutputItem, TResponseInputItem
16+
from .items import ReasoningItemIdPolicy, run_item_to_input_item
1617

1718
# --------------------------
1819
# Public helpers
@@ -55,12 +56,14 @@ def approvals_from_step(step: Any) -> list[ToolApprovalItem]:
5556
def append_input_items_excluding_approvals(
5657
base_input: list[TResponseInputItem],
5758
items: Sequence[RunItem],
59+
reasoning_item_id_policy: ReasoningItemIdPolicy | None = None,
5860
) -> None:
5961
"""Append tool outputs to model input while skipping approval placeholders."""
6062
for item in items:
61-
if item.type == "tool_approval_item":
63+
converted = run_item_to_input_item(item, reasoning_item_id_policy)
64+
if converted is None:
6265
continue
63-
base_input.append(item.to_input_item())
66+
base_input.append(converted)
6467

6568

6669
# --------------------------

src/agents/run_internal/error_handlers.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
MessageOutputItem,
1515
ModelResponse,
1616
RunItem,
17-
ToolApprovalItem,
1817
TResponseInputItem,
1918
)
2019
from ..models.fake_id import FAKE_RESPONSES_ID
@@ -25,6 +24,7 @@
2524
RunErrorHandlerResult,
2625
RunErrorHandlers,
2726
)
27+
from .items import ReasoningItemIdPolicy, run_item_to_input_item
2828
from .turn_preparation import get_output_schema
2929

3030

@@ -34,13 +34,15 @@ def build_run_error_data(
3434
new_items: list[RunItem],
3535
raw_responses: list[ModelResponse],
3636
last_agent: Agent[Any],
37+
reasoning_item_id_policy: ReasoningItemIdPolicy | None = None,
3738
) -> RunErrorData:
3839
history = ItemHelpers.input_to_new_input_list(input)
3940
output = []
4041
for item in new_items:
41-
if isinstance(item, ToolApprovalItem):
42+
converted = run_item_to_input_item(item, reasoning_item_id_policy)
43+
if converted is None:
4244
continue
43-
output.append(item.to_input_item())
45+
output.append(converted)
4446
history = history + list(output)
4547
return RunErrorData(
4648
input=input,

0 commit comments

Comments
 (0)