RunState round trip drops alias-only fields: `"async": null` on a function call is serialized as `async_` and breaks `get_interruptions()` on restore
Maintainers usually reply within 1 day
@iveteamorim is already working on this.
Since Oct 12, 2026.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 58/100
Research direction
Read the serialization and restore paths in agents/run_state.py: _serialize_raw_item_value, the ResponseFunctionToolCall reconstruction sites, and _copy_pydantic_value. Add a round-trip regression test for a function call with "async": null and run the relevant RunState serialization tests. Done means a serialized paused state restores and get_interruptions() succeeds with the alias-only field preserved.
Written by the indexing model from the issue text.
Description
Please read this first
- Have you read the docs? Yes (human-in-the-loop /
RunStateserialization). - Have you searched for related issues? Yes. #5162 (
created_bysurvivingmodel_dump(exclude_unset=True)and breaking replay) is the same class of problem at a different boundary; I found no issue about alias-only fields inRunStateround trips.
Describe the bug
RunState.to_string() / to_json() serialize raw output items with model_dump(exclude_unset=True) but without by_alias=True. For a field that only exists under an alias, the JSON ends up keyed by the Python attribute name, and from_string() / from_json() cannot read it back.
Concrete case: the Responses API now has an async flag on function calls, which openai models as ResponseFunctionToolCall.async_ (alias="async"). When a provider sends "async": null on a function call:
openaiparses it intoasync_ = Noneand marks the field as set.- The paused
RunStateis serialized as"async_": null(10 places in my dump, includingcurrent_step.data.interruptions[0].raw_item). - On restore,
ResponseFunctionToolCall(**data)only knows the aliasasync, soasync_lands inmodel_extra. get_interruptions()→_copy_tool_approval_raw_item→_copy_pydantic_valueraisesTypeError("Colliding key in tool approval model extras")because the extra key collides with a declared field, andget_interruptions()re-raises it asUserErrorwith the cause removed.
So a run can pause successfully and then be impossible to resume from its own serialized state. Pausing is the common HITL path, so a provider that includes this (or any future alias-only) field makes every resume fail deterministically.
Debug information
- Agents SDK version:
v0.22.2(also reproduced onv0.23.1, the latest release) - Related library versions:
openai3.13.0 (also 3.29.0), pydantic 2.13 - Python version: 3.12.14
- Operating system: Linux (WSL2)
- Model and model provider: any Responses-compatible provider that emits the
asyncflag on function calls; the repro below uses a customModelso no provider or API key is needed - Does the issue reproduce with the latest Agents SDK release? Yes, 0.23.1
- Does the issue occur consistently or intermittently? Consistently
serialized contains 'async_': True
Traceback (most recent call last):
File "/tmp/sdk-issue/repro.py", line 50, in <module>
asyncio.run(main())
File "/home/yangshuo/.local/share/micromamba/envs/storagent/lib/python3.12/asyncio/runners.py", line 195, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "/home/yangshuo/.local/share/micromamba/envs/storagent/lib/python3.12/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/yangshuo/.local/share/micromamba/envs/storagent/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^
File "/tmp/sdk-issue/repro.py", line 46, in main
restored.get_interruptions() # raises UserError
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/yangshuo/.local/share/micromamba/envs/storagent/lib/python3.12/site-packages/agents/run_state.py", line 1056, in get_interruptions
_raise_data_redacted_error(copy_error)
File "/home/yangshuo/.local/share/micromamba/envs/storagent/lib/python3.12/site-packages/agents/exceptions.py", line 410, in _raise_data_redacted_error
raise error from None
agents.exceptions.UserError: Cannot safely copy pending tool approvals. Ensure each interruption uses a supported tool call or contains only JSON-compatible mapping data.
Repro steps
"""RunState round-trip loses alias-only fields: `"async": null` -> `"async_"` -> UserError."""
import asyncio
from typing import Any
from openai.types.responses import ResponseFunctionToolCall
from agents import Agent, ModelResponse, RunConfig, Runner, RunState, function_tool
from agents.models.interface import Model
from agents.usage import Usage
class OneToolCallModel(Model):
async def get_response(self, *args: Any, **kwargs: Any) -> ModelResponse:
# What a Responses-compatible relay sends when it includes the new `async` flag.
call = ResponseFunctionToolCall.model_validate(
{
"id": "fc_1",
"call_id": "call_1",
"type": "function_call",
"name": "needs_ok",
"arguments": "{}",
"async": None,
}
)
return ModelResponse(output=[call], usage=Usage(), response_id="resp_1")
async def stream_response(self, *args: Any, **kwargs: Any):
raise NotImplementedError
yield
@function_tool(needs_approval=True)
async def needs_ok() -> str:
return "ok"
async def main() -> None:
agent = Agent(name="a", model=OneToolCallModel(), tools=[needs_ok])
result = await Runner.run(agent, "go", run_config=RunConfig(tracing_disabled=True))
assert result.interruptions, "expected a pending approval"
serialized = result.to_state().to_string()
print("serialized contains 'async_':", '"async_"' in serialized)
restored = await RunState.from_string(agent, serialized)
restored.get_interruptions() # raises UserError
print("restored OK")
asyncio.run(main())
Output on 0.22.2 and 0.23.1:
serialized contains 'async_': True
...
agents.exceptions.UserError: Cannot safely copy pending tool approvals. Ensure each interruption uses a supported tool call or contains only JSON-compatible mapping data.
Relevant code (0.22.2 line numbers; 0.23.1 is the same logic):
agents/run_state.py:2488_serialize_raw_item_value:model_dump(exclude_unset=True)withoutby_aliasagents/run_state.py:3170/:3225: restore viaResponseFunctionToolCall(**data), which only accepts the aliasagents/run_state.py:439_copy_pydantic_value:Colliding key in tool approval model extrasagents/run_state.py:1046-1056get_interruptions: wraps it intoUserErrorwithraise ... from None, so the originalTypeErroris not visible to callers
Expected behavior
A RunState produced by to_string() / to_json() restores and resumes unchanged. Either serialize raw items with by_alias=True (so the JSON matches the wire schema the models accept), or rebuild them with model_validate(..., by_name=True) / populate_by_name on restore. A dedicated round-trip test with an alias-only field ("async": null on a function call) would guard this.
Workaround I am using until then: strip a null async_ from model_fields_set on relayed function calls before they reach the Runner, and restore the serialized state once at pause time to fail early instead of at resume.
- Dominant language
- Python
- Stars
- 30k
- Forks
- 4.9k
- Avg merge
- 16h 25m
- Merged PRs (30d)
- 220
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from openai/openai-agents-python
-
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
openai/openai-agents-python#5348 · 3 comments ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 25/100
openai/openai-agents-python#5353 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 3/5 Half a day Newbie friendliness 70/100
openai/openai-agents-python#5342 · 4 comments ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 3/5 1-2 days Newbie friendliness 65/100
openai/openai-agents-python#5336 · 3 comments ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
openai/openai-agents-python#5322 · 2 comments ·
Maintainers usually reply within 1 day
All issues in openai/openai-agents-python
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
jbacalso24/mimry#291 ·
Maintainers usually reply within 1 day
-
good first issue
Difficulty 2/5 Under an hour Newbie friendliness 82/100
eli-alvarado/us-watchlist-alerts#14 ·
Maintainers usually reply within 1 day
-
from-review-extraction python severity:nit
Difficulty 2/5 1-3 hours Newbie friendliness 80/100
LearningCircuit/local-deep-research#7462 ·
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
cathrynlavery/diagram-design#441 ·
Maintainers usually reply within 2 days
-
[Cleanup] Include the public header in work_split.cppPossibly taken @blozano-tt claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
tenstorrent/tt-metal#60850 ·
Maintainers usually reply within 1 day