Skip to content

ComputerTool provider instances are not isolated across concurrent runs #3842

Description

@russeell

Please read this first

  • Have you read the docs? Yes, including the ComputerTool documentation.
  • Have you searched for related issues? Yes. I searched open and closed issues and pull requests for ComputerTool, ComputerProvider, per-run computer lifecycle, concurrent runs, and computer serialization. I found the original per-run lifecycle change in fix: Enable creating/disposing Computer per agent run #2191, but no report covering this concurrent serialization race.

Describe the bug

When two runs concurrently reuse an Agent containing the same ComputerTool backed by a ComputerProvider, the resolved Computer instances are cached per RunContextWrapper, but they are also written to the shared ComputerTool.computer field.

Reusing one configured Agent across concurrent application requests is a normal server pattern. PR #2191 introduced per-run computer creation and disposal specifically so provider-backed tools would not require rebuilding the agent for each request.

The preview-compatible Responses serializer reads that shared field to obtain environment and display dimensions. As a result, one run can serialize another run's computer configuration. If the first run completes and disposes its computer before the second run serializes, disposal restores the shared field to the provider and the second run fails with:

UserError: Computer tool is not initialized for serialization.

The GA computer payload does not require these dimensions, so the observed serialization failure is specific to preview-compatible payloads. However, the provider lifecycle is documented and implemented as per-run state, so concurrent runs should not share mutable resolved-computer state.

Debug information

  • Agents SDK version: v0.18.2 and main at 7369b73c
  • Python version: Python 3.13.13

Repro steps

From a repository checkout, save the following as repro_computer_concurrency.py and run it with uv run python repro_computer_concurrency.py. It uses a local fake model and makes no API request:

import asyncio
from typing import Any, cast

from openai.types.responses import ResponseOutputMessage, ResponseOutputText

from agents import (
    Agent,
    ComputerProvider,
    ComputerTool,
    RunConfig,
    Runner,
)
from agents.computer import Computer
from agents.models.openai_responses import Converter
from tests.fake_model import FakeModel


class DemoComputer(Computer):
    def __init__(self, width: int) -> None:
        self.width = width

    @property
    def environment(self):
        return "browser"

    @property
    def dimensions(self):
        return (self.width, 700)

    def screenshot(self): return "image"
    def click(self, *args): pass
    def double_click(self, *args): pass
    def scroll(self, *args): pass
    def type(self, *args): pass
    def wait(self): pass
    def move(self, *args): pass
    def keypress(self, *args): pass
    def drag(self, *args): pass


def message(text: str) -> ResponseOutputMessage:
    return ResponseOutputMessage(
        id="message",
        content=[ResponseOutputText(annotations=[], text=text, type="output_text")],
        role="assistant",
        status="completed",
        type="message",
    )


async def main() -> None:
    created: list[DemoComputer] = []
    disposed: list[DemoComputer] = []

    async def create(**kwargs: Any) -> DemoComputer:
        computer = DemoComputer(1001 + len(created))
        created.append(computer)
        return computer

    async def dispose(*, computer: DemoComputer, **kwargs: Any) -> None:
        disposed.append(computer)

    entered = [asyncio.Event(), asyncio.Event()]
    release = [asyncio.Event(), asyncio.Event()]
    serialized_widths: list[int] = []

    class GatedModel(FakeModel):
        def __init__(self) -> None:
            super().__init__(initial_output=[message("done")])
            self.set_next_output([message("done")])
            self.call_count = 0

        async def get_response(
            self,
            system_instructions,
            input,
            model_settings,
            tools,
            output_schema,
            handoffs,
            tracing,
            *,
            previous_response_id,
            conversation_id,
            prompt,
        ):
            index = self.call_count
            self.call_count += 1
            entered[index].set()
            await release[index].wait()

            converted = Converter.convert_tools(
                tools=tools,
                handoffs=handoffs,
                model="computer-use-preview",
            )
            payload = cast(dict[str, Any], converted.tools[0])
            serialized_widths.append(cast(int, payload["display_width"]))

            return await super().get_response(
                system_instructions,
                input,
                model_settings,
                tools,
                output_schema,
                handoffs,
                tracing,
                previous_response_id=previous_response_id,
                conversation_id=conversation_id,
                prompt=prompt,
            )

    tool = ComputerTool(
        computer=ComputerProvider(create=create, dispose=dispose)
    )
    agent = Agent(name="computer", model=GatedModel(), tools=[tool])
    config = RunConfig(tracing_disabled=True)

    task_a = asyncio.create_task(Runner.run(agent, "run-a", run_config=config))
    await entered[0].wait()
    task_b = asyncio.create_task(Runner.run(agent, "run-b", run_config=config))
    await entered[1].wait()

    release[0].set()
    result_a = await task_a
    release[1].set()

    print("run A:", result_a.final_output)
    try:
        result_b = await task_b
        print("run B:", result_b.final_output)
    except Exception as error:
        print("run B failed:", type(error).__name__, error)

    print("serialized widths:", serialized_widths)
    print("disposed widths:", [computer.width for computer in disposed])


asyncio.run(main())

Observed output:

run A: done
run B failed: UserError Computer tool is not initialized for serialization. ...
serialized widths: [1002]
disposed widths: [1001, 1002]

Run A serializes run B's width. When run A then performs cleanup, run B sees the provider instead of its resolved computer and fails during serialization.

Expected behavior

Each run should pass its own resolved Computer instance to model serialization and tool execution. Concurrent runs that reuse the same Agent and ComputerTool should serialize their own dimensions, complete independently, and dispose exactly their own provider-created computer once.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions