Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

RunState round trip drops alias-only fields: `"async": null` on a function call is serialized as `async_` and breaks `get_interruptions()` on restore

Aperta
#5,352 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@iveteamorim ci sta già lavorando.

Dal 12/10/2026.

Valutazione

Difficoltà
3/5
Tempo stimato
1-2 giorni
Idoneità per principianti
58/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
python
Ambito
ai, backend

Direzione di ricerca

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.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Please read this first
  • Have you read the docs? Yes (human-in-the-loop / RunState serialization).
  • Have you searched for related issues? Yes. #5162 (created_by surviving model_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 in RunState round 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:

  1. openai parses it into async_ = None and marks the field as set.
  2. The paused RunState is serialized as "async_": null (10 places in my dump, including current_step.data.interruptions[0].raw_item).
  3. On restore, ResponseFunctionToolCall(**data) only knows the alias async, so async_ lands in model_extra.
  4. get_interruptions() → _copy_tool_approval_raw_item → _copy_pydantic_value raises TypeError("Colliding key in tool approval model extras") because the extra key collides with a declared field, and get_interruptions() re-raises it as UserError with 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 on v0.23.1, the latest release)
  • Related library versions: openai 3.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 async flag on function calls; the repro below uses a custom Model so 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) without by_alias
  • agents/run_state.py:3170 / :3225: restore via ResponseFunctionToolCall(**data), which only accepts the alias
  • agents/run_state.py:439 _copy_pydantic_value: Colliding key in tool approval model extras
  • agents/run_state.py:1046-1056 get_interruptions: wraps it into UserError with raise ... from None, so the original TypeError is 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.

Lingua principale
Python
Stelle
29.9k
Fork
4.8k
Merge medio
22h 17m
PR unite (30g)
208

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di openai/openai-agents-python

Tutte le issue di openai/openai-agents-python

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.