Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
eaf8c30
fix(sessions): persist deferred interrupted-turn items when the appro…
dixso Sep 2, 2026
9245717
refactor: share the deferred-prefix selection between both resume paths
dixso Sep 2, 2026
6c3174c
fix: derive the park-time deferral decision from checkpoint state
dixso Sep 2, 2026
449e91a
fix: carry the deferred prefix through a streamed re-interruption
dixso Sep 2, 2026
313b8c0
fix: confirm the deferred prefix against the Session and carry it to …
dixso Sep 2, 2026
3b3cd8b
fix: suppress only what the Session provably holds, by collision-free…
dixso Sep 2, 2026
b5b63f3
fix: recognize hosted MCP approval identities when reconciling the pr…
dixso Sep 2, 2026
47d3852
test: pin that an emptied resolved turn corrupts nothing in either ru…
dixso Sep 4, 2026
0d485af
fix(sessions): declare the withheld interrupted write as a held pendi…
dixso Sep 5, 2026
e7df96d
test: cover the held pending write across both runners and its serial…
dixso Sep 5, 2026
86b5f7f
refactor(sessions): drop the dead no-state park bridge and tighten de…
dixso Sep 5, 2026
b95d567
test: pin that a settled or discarded batch never lingers on the live…
dixso Sep 5, 2026
529b930
style: apply ruff formatting to the touched files
dixso Sep 5, 2026
f31635a
fix(sessions): narrow the fresh park registration for the type checker
dixso Sep 5, 2026
f8785fd
fix(sessions): validate the held resume's Session and settle only pai…
dixso Sep 5, 2026
546a3b4
style: satisfy line length and the settling batch parameter type
dixso Sep 5, 2026
d7bbe1a
fix(sessions): carry the held batch through detached parks, pending a…
dixso Sep 5, 2026
d4d70d9
fix(sessions): keep run input out of the held batch, register final s…
dixso Sep 5, 2026
0ce31cd
fix(sessions): dedupe the rebuilt final response against the batch an…
dixso Sep 5, 2026
1b916c7
fix(sessions): settle held batches through the canonical pairing and …
dixso Sep 5, 2026
ca6dd82
fix(sessions): keep crash recovery armed when the rebuilt final items…
dixso Sep 5, 2026
ab167f5
fix(sessions): give the held pending write its own schema version, se…
dixso Sep 7, 2026
b1775aa
fix(sessions): defer compaction when the settling batch carries the t…
dixso Sep 7, 2026
25ee168
fix(sessions): make the 1.18 corpus entries reproducible and scope th…
dixso Sep 7, 2026
9fed9ca
test: pin that the held-only keys are refused on an ordinary pending …
dixso Sep 7, 2026
fd6311c
fix(sessions): settle the pairs an emptied resolved turn leaves behind
dixso Sep 7, 2026
b498f4b
fix(sessions): dispose of the held batch when the run ends, not when …
dixso Sep 7, 2026
58ac099
fix(sessions): count and classify the settling batch on the compactio…
dixso Sep 8, 2026
33651b1
fix(sessions): the held record owns the conversion policy of its items
dixso Sep 8, 2026
0170048
fix(sessions): the final sweep's direct settle speaks the settle dialect
dixso Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/agents/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
ProcessedResponse,
QueueCompleteSentinel,
)
from .run_state import RunState
from .run_state import RunState, _PendingSessionWrite
from .stream_events import StreamEvent
from .tool_guardrails import ToolInputGuardrailResult, ToolOutputGuardrailResult
from .tracing import Trace
Expand Down Expand Up @@ -156,6 +156,9 @@ def _populate_state_from_result(
else:
state._generated_prompt_cache_key = getattr(result, "_generated_prompt_cache_key", None)
state._pending_input = copy.deepcopy(getattr(result, "_pending_input_for_state", []))
state._pending_session_write = copy.deepcopy(
getattr(result, "_pending_session_write", None)
)
state._current_step = getattr(result, "_current_step_for_state", None)
state._reasoning_item_id_policy = getattr(result, "_reasoning_item_id_policy", None)

Expand Down Expand Up @@ -367,6 +370,10 @@ class RunResultBase(abc.ABC):
default_factory=list, init=False, repr=False
)
"""Pending input preserved when a non-streaming result is converted back to RunState."""
_pending_session_write: _PendingSessionWrite | None = field(
default=None, init=False, repr=False
)
"""Held pending Session write preserved when a non-streaming result becomes a RunState."""
_current_step_for_state: Any = field(default=None, init=False, repr=False)
"""Current step preserved when a non-streaming result is converted back to RunState."""

Expand Down
177 changes: 144 additions & 33 deletions src/agents/run.py

Large diffs are not rendered by default.

51 changes: 46 additions & 5 deletions src/agents/run_internal/agent_runner_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from __future__ import annotations

from collections.abc import Mapping
import copy
from collections.abc import Mapping, Sequence
from typing import Any, cast

from openai.types.responses.response_usage import OutputTokensDetails
Expand Down Expand Up @@ -41,7 +42,13 @@
NextStepRunAgain,
ProcessedResponse,
)
from .session_persistence import save_result_to_session, save_resumed_turn_items
from .session_persistence import (
_held_items_safe_to_settle,
_pending_approval_call_ids,
final_items_cover_held_batch,
save_result_to_session,
save_resumed_turn_items,
)
from .tool_use_tracker import AgentToolUseTracker, serialize_tool_use_tracker
from .turn_preparation import get_model

Expand Down Expand Up @@ -488,6 +495,10 @@ def build_interruption_result(
if run_state is not None:
result._current_turn_persisted_item_count = run_state._current_turn_persisted_item_count
result._trace_state = run_state._trace_state
# The held pending write must survive the result checkpoint: a non-streamed
# caller serializes ``result.to_state()``, which has no live ``RunState`` to
# read the declaration from.
result._pending_session_write = copy.deepcopy(run_state._pending_session_write)
result._original_input = copy_input_items(original_input)
return result

Expand Down Expand Up @@ -582,13 +593,25 @@ async def save_final_turn_items_after_guardrails(
reasoning_item_id_policy: ReasoningItemIdPolicy | None = None,
store: bool | None = None,
wrapper: RunContextWrapper[Any] | None = None,
held_input: Sequence[TResponseInputItem] | None = None,
) -> int:
"""Persist deferred final-turn items without skipping a partially persisted resumed turn."""
if not session_persistence_enabled or not items:
"""Persist deferred final-turn items without skipping a partially persisted resumed turn.

``held_input`` is a claimed held batch that must land ahead of the final items in
the same append. It is safe to pass even when the rebuilt final items already
contain the parked response: the save deduplicates the combined batch.
"""
if not session_persistence_enabled or (not items and not held_input):
return 0
if input_guardrails_triggered(input_guardrail_results):
return 0
# Whether a held batch is being claimed at all, captured before any dedup empties
# it: the recovery registration below must stay armed even when the guardrail
# rebuild already carries the batch.
settling_held = bool(held_input)
if run_state is not None and run_state._current_turn_persisted_item_count > 0:
# save_resumed_turn_items owns the dedup, pairing, and recovery arming; the raw
# held batch rides in so it can arm from its own pre-dedup view.
run_state._current_turn_persisted_item_count = await save_resumed_turn_items(
session=session,
items=items,
Expand All @@ -597,17 +620,35 @@ async def save_final_turn_items_after_guardrails(
reasoning_item_id_policy=run_state._reasoning_item_id_policy,
store=store,
wrapper=wrapper,
run_state=run_state,
held_input=held_input,
)
return run_state._current_turn_persisted_item_count
if held_input and final_items_cover_held_batch(items, held_input, reasoning_item_id_policy):
# The guardrail rebuild re-derived the whole current response, held requests
# included; feeding the batch again would duplicate its unkeyed companions.
held_input = None
if held_input:
held_input = _held_items_safe_to_settle(
held_input,
items,
reasoning_item_id_policy,
pending_call_ids=_pending_approval_call_ids(run_state),
)
return await save_result_to_session(
session,
[],
list(held_input) if held_input else [],
Comment thread
dixso marked this conversation as resolved.
list(items),
run_state,
response_id=response_id,
reasoning_item_id_policy=reasoning_item_id_policy,
store=store,
wrapper=wrapper,
# A settling held batch always registers, so a crash inside this append fails
# closed with the batch recorded instead of silently losing it, even when the
# payload was deduplicated from the append.
resumed_write_state=run_state if settling_held else None,
settling_held_batch=settling_held,
)


Expand Down
Loading