Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

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

Cerrado
#5,352 1 comentario 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

@iveteamorim ya está trabajando en esto.

Desde el 12/10/2026.

Evaluación

Dificultad
3/5
Tiempo estimado
1-2 días
Aptitud para principiantes
58/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
python
Área
ai, backend

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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.

Lenguaje dominante
Python
Estrellas
30k
Forks
4.9k
Merge medio
18 h 13 min
PR fusionados (30 d)
215

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de openai/openai-agents-python

Todos los issues de openai/openai-agents-python

Issues similares

Más issues de Python

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.